To use the DOM, you need to know about
windows, documents,
elements, and nodes.
A Window object
represents the actual window of the web browser. Each Window has a
document property (a Document
object), which points to the document currently loaded. The Window
object also has accessors to various APIs such as IndexedDB (for storing
data), requestAnimationFrame() (for animations), and
more. In tabbed browsers, each tab has its own Window object.
With the
Document object, you can create and manipulate Elements within
the document. Note that the document itself is an element and can be
manipulated.
The DOM models a tree of Nodes. These nodes
are often elements, but they can also be attributes, text, comments, and
other DOM types. Except for the root node, which has no parent, each
node in the DOM has one parent and might have many children.
To manipulate an element, you first need an object that
represents it. You can get this object using a query.
Find one or more elements using the top-level functions
query() and queryAll(). You can
query by ID, class, tag, name, or any combination of these. The CSS Selector Specification
guide defines the formats of the selectors such as using a #
prefix to specify IDs and a period (.) for classes.
The query() function returns the first element that matches the
selector, while queryAll() returns a
collection of elements that match the selector.
Element elem1 = query('#an-id'); // Find an element by id (an-id).
Element elem2 = query('.a-class'); // Find an element by class (a-class).
List<Element> elems1 = queryAll('div'); // Find all elements by tag (<div>).
List<Element> elems2 = queryAll('input[type="text"]'); // Find all text inputs.
// Find all elements with the CSS class 'class' inside of a <p>
// that is inside an element with the ID 'id'.
List<Element> elems3 = queryAll('#id p.class');You can use properties to change the state of an element. Node
and its subtype Element define the properties that all elements have.
For example, all elements have classes, hidden, id, innerHtml, style, text, and title properties. Subclasses of Element
define additional properties, such as the href property of AnchorElement.
Consider this example of specifying an anchor element in
HTML:
<a id="example" href="http://example.com">link text</a>
This <a> tag specifies an element with an href attribute and a text node (accessible
via a text property) that contains
the string “linktext”. To change the URL that the link goes to, you
can use AnchorElement’s href
property:
query('#example').href = 'http://dartlang.org';Often you need to set properties on multiple elements. For
example, the following code sets the hidden property of all elements that have a
class of “mac”, “win”, or “linux”. Setting the hidden property to true has the same effect
as adding display:none to the CSS.
<!-- In HTML: -->
<p>
<span class="os linux">Words for Linux</span>
<span class="os mac">Words for Mac</span>
<span class="os win">Words for Windows</span>
</p>
// In Dart:
final osList = ['mac', 'win', 'linux'];
var userOs = 'linux'; // In real code you'd programmatically determine this.
for (var os in osList) { // For each possible OS...
bool shouldShow = (os == userOs); // Does this OS match the user's OS?
for (var elem in queryAll('.$os')) { // Find all elements for this OS.
elem.hidden = !shouldShow; // Show or hide each element.
}
}When the right property isn’t available or convenient, you can
use Element’s attributes property.
This property has the type AttributeMap,
which implements a map with keys
that are strings (attribute names) and values that it automatically
converts to strings. For a list of attribute names and their meanings,
see the
MDN Attributes page. Here’s an example of setting an
attribute’s value:
elem.attributes['someAttribute'] = 'someValue';
You can add to existing HTML pages by creating new elements and
attaching them to the DOM. Here’s an example of creating a paragraph
(<p>) element:
var elem = new ParagraphElement();
elem.text = 'Creating is easy!';
You can also create an element by parsing HTML text. Any child
elements are also parsed and created.
var elem2 = new Element.html('<p>Creating <em>is</em> easy!</p>');Note that elem2 is a ParagraphElement in the above
example.
Attach the newly created element to the document by assigning a
parent to the element. You can add an element to any existing
element’s children. In the following example, body is an element, and its child elements
are accessible (as a List<Element>) from the children property.
document.body.children.add(elem2);
Adding, replacing, and removing nodes
Recall that elements are just a kind of node. You can find all
the children of a node using the nodes property of Node, which returns a
List<Node>. Once you have this list, you can use the usual List
methods and operators to manipulate the children of the node.
To add a node as the last child of its parent, use the List
add() method:
// Find the parent by ID, and add elem as its last child.
query('#inputs').nodes.add(elem);To replace a node, use the Node replaceWith()
method:
// Find a node by ID, and replace it in the DOM.
query('#status').replaceWith(elem);To remove a node, use the Node remove()
method:
// Find a node by ID, and remove it from the DOM.
query('#expendable').remove();CSS, or cascading style sheets, is used to
define the presentation styles of DOM elements. You can change the
appearance of an element by attaching ID and class attributes to
it.
Each element has a classes
field, which is a list. Add and remove CSS classes simply by adding
and removing strings from this collection. For example, the following
sample adds the warning class to an
element:
var element = query('#message');
element.classes.add('warning');It’s often very efficient to find an element by ID. You can
dynamically set an element ID with the id property:
var message = new DivElement();
message.id = 'message2';
message.text = 'Please subscribe to the Dart mailing list.';
You can reduce the redundant text in this example by using
method cascades:
var message = new DivElement()
..id = 'message2'
..text = 'Please subscribe to the Dart mailing list.';While using IDs and classes to associate an element with a set
of styles is best practice, sometimes you want to attach a specific
style directly to the element:
message.style
..fontWeight = 'bold'
..fontSize = '3em';To respond to external events such as clicks, changes of focus,
and selections, add an event listener. You can add an event listener
to any element on the page. Event dispatch and propagation is a
complicated subject; research
the details if you’re new to web programming.
Add an event handler using element.onEvent.listen(function),
where Event is the
event name and function is the
event handler.
For example, here’s how you can handle clicks on a
button:
// Find a button by ID and add an event handler.
query('#submitInfo').onClick.listen((e) {
// When the button is clicked, it runs this code.
submitData();
});Events can propagate up and down through the DOM tree. To
discover which element originally fired the event, use e.target:
document.body.onClick.listen((e) {
var clickedElem = e.target;
print('You clicked the ${clickedElem.id} element.');
});To see all the events for which you can register an event
listener, consult the API docs for ElementEvents
and its subclasses. Some common events include:
change
blur
keyDown
keyUp
mouseDown
mouseUp
Using HTTP Resources with HttpRequest
Formerly known as XMLHttpRequest, the HttpRequest
class gives you access to HTTP resources from within your browser-based
app. Traditionally, AJAX-style apps make heavy use of HttpRequest. Use
HttpRequest to dynamically load JSON data or any other resource from a
web server. You can also dynamically send data to a web server.
The following examples assume all resources are served from the
same web server that hosts the script itself. Due to security
restrictions in the browser, the HttpRequest class can’t easily use
resources that are hosted on an origin that is different from the origin
of the app. If you need to access resources that live on a different web
server, you need to either use a technique called JSONP or enable CORS
headers on the remote resources.
Getting data from the server
The HttpRequest static method getString() is
an easy way to get data from a web server. Use
then() after getString() to
specify the function that handles the string data.
import 'dart:html';
import 'dart:async';
// A JSON-formatted file in the same location as this page.
var uri = 'data.json';
main() {
// Read a JSON file.
HttpRequest.getString(uri).then(processString);
}
processString(String jsonText) {
parseText(jsonText);
}The function you specify (in the example,
processString()) runs when the data at the
specified URI is successfully retrieved. In this case, we are
dynamically loading a JSON file. Information about the JSON API is in
the section called “dart:json - Encoding and Decoding Objects”.
Add .catchError() after the call to
.then() to specify an error handler:
...
HttpRequest.getString(uri)
.then(processString)
.catchError(handleError);
...
handleError(error) {
print('Uh oh, there was an error.');
print(error.toString());
}If you need access to the HttpRequest, not just the text data it
retrieves, you can use the request() static method
instead of getString(). Here’s an example of
reading XML data:
import 'dart:html';
import 'dart:async';
// An XML-formatted file in the same location as this page.
var xmlUri = 'data.xml';
main() {
// Read an XML file.
HttpRequest.request(uri)
.then(processRequest)
.catchError(handleError);
}
processRequest(HttpRequest request) {
var xmlDoc = request.responseXml;
try {
var license = xmlDoc.query('license').text;
print('License: $license');
} catch(e) {
print('$xmlUri doesn\'t have correct XML formatting.');
}
}
...You can also use the full API to handle more interesting cases.
For example, you can set arbitrary headers.
The general flow for using the full API of HttpRequest is as
follows:
Create the HttpRequest object.
Open the URL with either GET or POST.
Attach event handlers.
Send the request.
For example:
import 'dart:html';
...
var httpRequest = new HttpRequest();
httpRequest.open('POST', dataUrl);
httpRequest.onLoadEnd.listen((e) => loadEnd(httpRequest));
httpRequest.send(encodedData);
Sending data to the server
HttpRequest can send data to the server using the HTTP method
POST. For example, you might want to dynamically submit data to a form
handler. Sending JSON data to a RESTful web service is another common
example.
Submitting data to a form handler requires you to provide
name-value pairs as URI-encoded strings. (Information about the URI
class is in the section called “URIs”.) You must also set the
Content-type header to application/x-www-form-urlencode if you wish
to send data to a form handler.
import 'dart:html';
String encodeMap(Map data) {
return data.keys.map((k) {
return '${Uri.encodeComponent(k)}=${Uri.encodeComponent(data[k])}';
}).join('&');
}
loadEnd(HttpRequest request) {
if (request.status != 200) {
print('Uh oh, there was an error of ${request.status}');
} else {
print('Data has been posted');
}
}
main() {
var dataUrl = '/registrations/create';
var data = {'dart': 'fun', 'editor': 'productive'};
var encodedData = encodeMap(data);
var httpRequest = new HttpRequest();
httpRequest.open('POST', dataUrl);
httpRequest.setRequestHeader('Content-type',
'application/x-www-form-urlencoded');
httpRequest.onLoadEnd.listen((e) => loadEnd(httpRequest));
httpRequest.send(encodedData);
}Sending and Receiving Real-Time Data with WebSockets
A WebSocket allows your web app to exchange data with a server
interactively—no polling necessary. A server creates the WebSocket and
listens for requests on a URL that starts with ws://—for example, ws://127.0.0.1:1337/ws. The
data transmitted over a WebSocket can be a string, a blob, or an ArrayBuffer.
Often, the data is a JSON-formatted string.
To use a WebSocket in your web app, first create a WebSocket
object, passing the WebSocket URL as an argument:
var webSocket = new WebSocket('ws://127.0.0.1:1337/ws');To send string data on the WebSocket, use the
send() method:
sendMessage(String data) {
if (webSocket.readyState == WebSocket.OPEN) {
webSocket.send(data);
} else {
throw 'WebSocket not connected, message $data not sent';
}
}To receive data on the WebSocket, register a listener for
message events:
webSocket.onMessage.listen((MessageEvent e) {
receivedMessage(e.data);
});The message event handler receives a MessageEvent
object. This object’s data field
has the data from the server. Here’s an example of decoding a JSON
string sent on a WebSocket, where the JSON string has two fields,
“from” and “content”:
// Called from the message listener like this: receivedMessage(e.data)
receivedMessage(String data) {
Map message = json.parse(data);
if (message['from'] != null) {
print('Message from ${message['from']}: ${message['content']}');
}
}Handling WebSocket events
WebSocketEvents
defines the WebSocket events your app can handle: open, close, error,
and (as shown above) message. Here’s an example of a method that
creates a WebSocket object and handles message, open, close, and error
events:
connectToWebSocket([int retrySeconds = 2]) {
bool reconnectScheduled = false;
webSocket = new WebSocket(url);
scheduleReconnect() {
print('web socket closed, retrying in $retrySeconds seconds');
if (!reconnectScheduled) {
new Timer(new Duration(seconds:retrySeconds),
() => connectToWebSocket(retrySeconds*2));
}
reconnectScheduled = true;
}
webSocket.onOpen.listen((e) {
print('Connected');
});
webSocket.onClose.listen((e) => scheduleReconnect());
webSocket.onError.listen((e) => scheduleReconnect());
webSocket.onMessage.listen((MessageEvent e) {
_receivedEncodedMessage(e.data);
});
}For an example of using WebSockets, see Chapter 5, Walkthrough: Dart Chat.