Low-Level HTML Pirate Code Lab

Send feedback

This code lab introduces you to the Dart language and libraries by walking you through the process of building a simple web app.

All you need is a browser and some familiarity with programming. You’ll use the browser to run DartPad, a tool for writing and running simple Dart apps. (By the way, both the client and the server side of DartPad are themselves Dart apps.)

Build this app!


 


Step 1: Run the skeleton app

In this step, you run the skeleton version of the app in DartPad.

Run the app in DartPad.

Open the skeleton app in DartPad.

Click the Run button and make sure that the HTML OUTPUT tab is selected. You should see a red-and-white name badge, plus some text, as shown in the following screenshot:

A screenshot of the output created in the skeleton app

DartPad is an interactive web app that lets you immediately play with Dart in your browser without having to download or install any software, or do any special setup. It just works!

The following screenshot shows DartPad’s UI. This example contains an intentional error to show you how DartPad handles problems that it detects.

A screenshot of DartPad running the skeleton version of the client code

The code appears on the left under the DART, HTML, or CSS tab. Output appears on the right under the HTML OUTPUT or CONSOLE tab. You can always restart the app using the Run button. If the analyzer detects problems, errors and warnings appear at the bottom of the screen.


Review the code.

Get familiar with the Dart and HTML code (in main.dart and index.html, respectively) for the skeleton version of the app.

main.dart

void main() {
  // Your app starts here.
  // In Step 2, you'll add code to listen for updates for the pirate badge.
}

Key information

  • The main() function is the single entry point for the app.

  • main() is a top-level function.

  • A top-level variable or function is one that is declared outside a class definition.

 

index.html

<h1>Pirate badge</h1>

<div class="widgets">
  TO DO: Put the UI widgets here.
</div>
<div class="badge">
  <div class="greeting">
    Arrr! Me name is
  </div>
  <div class="name">
    <span id="badgeName"> </span>
  </div>
</div>
  • All of the changes you make to the HTML in this code lab are within the <div> element identified with the class widgets.

  • In later steps, the <span> element with the ID badgeName is programmatically updated by the Dart code based on user input.

Step 2: Add an input field

In this step, you add an input field to the app. As the user types into the text field, the Dart code updates the badge from the value of the text field.

Edit the Dart code.

Import the dart:html library at the top of the code, below the copyright.

import 'dart:html';

Key information

  • This imports all classes and other resources from dart:html.

  • The dart:html library contains the classes for all DOM element types, in addition to functions for accessing the DOM.

  • Later you’ll use import with the show keyword, which imports only the specified classes.

  • DartPad helpfully warns you that the import is unused. Don’t worry about it. You’ll fix it in the next step.


Register a function to handle input events on the input field.

void main() {
  querySelector('#inputName').onInput.listen(updateBadge);
}
  • The querySelector() function, defined in dart:html, gets the specified element from the DOM. Here, the code uses the selector #inputName to specify the input field.

  • The object returned from querySelector() is the DOM element object.

  • Mouse and keyboard events are served over a stream.

  • A Stream provides an asynchronous sequence of data. Using the listen() method, a stream client registers a callback function that gets called when data is available.

  • onInput.listen() listens to the text field’s event stream for input events. When such an event occurs, updateBadge() is called.

  • An input event occurs when the user presses a key.

  • You can use either single or double quotes to create a string.

  • DartPad warns you that updateBadge doesn’t exist. Let’s fix that now.


Implement the event handler as a top-level function.

...

void updateBadge(Event e) {
  querySelector('#badgeName').text = e.target.value;
}
  • This function sets the text of the badgeName element from the value of the input field.

  • Event e is the argument to the updateBadge function. The argument’s name is e; its type is Event.

  • You can tell that updateBadge() is an event handler because its parameter is an Event object.

  • The element that generated the event, the input field, is e.target.

  • DartPad prints the warning: The getter ‘value’ is not defined for the class ‘EventTarget’. You fix that in the next step.

 


Fix the warning message.

...

void updateBadge(Event e) {
  querySelector('#badgeName').text = (e.target as InputElement).value;
}
  • In this example, e.target is the input element that generated the event.

  • InputElement is one of many different kinds of DOM elements provided by the dart:html library.

  • The as keyword typecasts e.target to an InputElement to silence warnings from DartPad.

 

Edit the HTML code.

Replace the “TO DO” line in the HTML code with the <input> tag.

...
<div class="widgets">
  <div>
    <input type="text" id="inputName" maxlength="15">
  </div>
</div>
...
  • The ID for the input element is inputName. Dart uses CSS selectors, such as #inputName, to get elements from the DOM.

 

Test it!

Click the Run button.

Type in the input field. The name badge updates to display what you’ve typed.

Problems?

Check your code against the solution.

Or open the solution in DartPad.

You may notice that the HTML displayed in DartPad is a bit different than what is shown on GitHub. DartPad requires only the portion of the HTML specific to the example— what appears between the <body> tags in the full HTML file.


Step 3: Add a button

In this step, you add a button to the app. The button is enabled when the text field contains no text. When the user clicks the button, the app puts the name Anne Bonney on the badge.

Edit the Dart code.

Below the import, declare a top-level variable to hold the ButtonElement.

import 'dart:html';

ButtonElement genButton;

Key information

  • Variables, including numbers, initialize to null if no value is provided.

 


Wire up the button with an event handler.

void main() {
  querySelector('#inputName').onInput.listen(updateBadge);
  genButton = querySelector('#generateButton');
  genButton.onClick.listen(generateBadge);
}
  • onClick registers a mouse click handler.

 


Add a top-level function that changes the name on the badge.

...

void setBadgeName(String newName) {
  querySelector('#badgeName').text = newName;
}
  • The function updates the HTML page with a new name.

 


Implement the click handler for the button.

...

void generateBadge(Event e) {
  setBadgeName('Anne Bonney');
}
  • This function sets the badge name to Anne Bonney.

 


Modify updateBadge() to call setBadgeName().

void updateBadge(Event e) {
  String inputName = (e.target as InputElement).value;
  setBadgeName(inputName);
}
  • Assign the input field’s value to a local string.

Add a skeleton if-else statement to updateBadge().

void updateBadge(Event e) {
  String inputName = (e.target as InputElement).value;
  setBadgeName(inputName);
  if (inputName.trim().isEmpty) {
    // To do: add some code here.
  } else {
    // To do: add some code here.
  }
}
  • The String class has useful functions and properties for working with string data, such as trim() and isEmpty.

  • String comes from the dart:core library, which is automatically imported into every Dart program.

  • Dart has common programming language constructs like if-else.


Now fill in the if-else statement to modify the button as needed.

void updateBadge(Event e) {
  String inputName = (e.target as InputElement).value;
  setBadgeName(inputName);
  if (inputName.trim().isEmpty) {
    genButton..disabled = false
             ..text = 'Aye! Gimme a name!';
  } else {
    genButton..disabled = true
             ..text = 'Arrr! Write yer name!';
  }
}
  • The cascade operator (..) allows you to perform multiple operations on the members of a single object.

  • The updateBadge() code uses the cascade operator to set two properties on the button element. The result is the same as this more verbose code:

    genButton.disabled = false;
    genButton.text = 'Aye! Gimme a name!';
    

Edit the HTML code.

Add the <button> tag below the input field.

...
<div class="widgets">
  <div>
    <input type="text" id="inputName" maxlength="15">
  </div>
  <div>
    <button id="generateButton">Aye! Gimme a name!</button>
  </div>
</div>
...
  • The button has the ID generateButton so the Dart code can get the element.

Test it!

Click the Run button.

Type in the input field. Remove the text from the input field. Click the button.

Problems?

Check your code against the solution.

Or open the solution in DartPad.


Step 4: Create a class

In this step, you create a class to represent a pirate name. When created, an instance of this class randomly selects a name and appellation from a list, or optionally you can provide a name and an appellation to the constructor.

Edit the Dart code.

Add an import at the top of the code.

import 'dart:html';
import 'dart:math' show Random;

Key information

  • Using the show keyword, you can import only the classes, functions, or properties you need.

  • Random provides a random number generator.


Add a class declaration to the bottom of the code.

...

class PirateName {
}
  • The class declaration provides the class name.

Create a class-level Random object.

class PirateName {
  static final Random indexGen = new Random();
}
  • static defines a class-level field. That is, the random number generator is shared with all instances of this class.

  • Use new to call a constructor.


Add two instance variables to the class, one for the first name and one for the appellation.

class PirateName {
  static final Random indexGen = new Random();

  String _firstName;
  String _appellation;
}
  • Private variables start with underscore (_). Dart has no private keyword.

Create two static lists within the class that provide a small collection of names and appellations to choose from.

class PirateName {
  ...

  static final List names = [
    'Anne', 'Mary', 'Jack', 'Morgan', 'Roger',
    'Bill', 'Ragnar', 'Ed', 'John', 'Jane' ];
  static final List appellations = [
    'Jackal', 'King', 'Red', 'Stalwart', 'Axe',
    'Young', 'Brave', 'Eager', 'Wily', 'Zesty'];
  • final variables cannot change.

  • Lists are built into the language. These lists are created using list literals.

  • The List class provides the API for lists.


Provide a constructor for the class.

class PirateName {
  ...
  PirateName({String firstName, String appellation}) {
    if (firstName == null) {
      _firstName = names[indexGen.nextInt(names.length)];
    } else {
      _firstName = firstName;
    }
    if (appellation == null) {
      _appellation =
          appellations[indexGen.nextInt(appellations.length)];
    } else {
      _appellation = appellation;
    }
  }
}
  • Constructor names can be either ClassName or ClassName.identifier.

  • The parameters enclosed in curly brackets ({ and }) are optional, named parameters.

  • The nextInt() function gets a new random integer from the random number generator.

  • Use square brackets ([ and ]) to index into a list.

  • The length property returns the number of items in a list.

  • The code uses a random number as an index into the list.


Provide a getter for the pirate name.

class PirateName {
  ...
  String get pirateName =>
      _firstName.isEmpty ? '' : '$_firstName the $_appellation';
}
  • Getters are special methods that provide read access to an object’s properties.

  • The conditional operator ?: is equivalent to an if-then-else statement, but for expressions.

  • String interpolation ('$_firstName the $_appellation') lets you easily build strings from other objects.

  • The fat arrow ( => expr; ) syntax is a shorthand for { return expr; }.


Override the toString() method.

class PirateName {
  ...
  String toString() => pirateName;
}
  • Because the Object implementation of toString() doesn’t give much information, many classes override toString().

  • When you call print(anObject) for any non-String, it prints the value returned by anObject.toString().

  • Overriding toString() can be especially helpful for debugging or logging.


Modify the function setBadgeName() to use a PirateName instead of a String:

void setBadgeName(PirateName newName) {
  querySelector('#badgeName').text = newName.pirateName;
}
  • This code calls the getter to get the PirateName as a string.

Change updateBadge() to generate a PirateName based on the input field value.

void updateBadge(Event e) {
  String inputName = (e.target as InputElement).value;

  setBadgeName(new PirateName(firstName: inputName));
  ...
}
  • The call to the constructor provides a value for one optional named parameter.

Change generateBadge() to generate a PirateName instead of using Anne Bonney.

void generateBadge(Event e) {
  setBadgeName(new PirateName());
}
  • In this case, the call to the constructor passes no parameters.

Test it!

Click the Run button.

Type in the input field. Remove the text from the input field. Click the button.

Problems?

Check your code against the solution.

Or open the solution in DartPad.


Step 5: Read a JSON file

In this step, you change the PirateName class to get the list of names and appellations from a JSON file on dartlang.

Edit the Dart code.

Add imports to the top.

import 'dart:html';
import 'dart:math' show Random;
import 'dart:convert' show JSON;
import 'dart:async' show Future;

Key information

  • JSON provides convenient access to the most commonly used JSON conversion utilities.

  • The dart:async library provides for asynchronous programming.

  • A Future provides a way to get a value in the future. (For JavaScript developers: Futures are similar to Promises.)


Add a getter to the PirateName class that encodes a pirate name in a JSON string.

class PirateName {
  ...
  String get jsonString =>
      JSON.encode({"f": _firstName, "a": _appellation});
}
  • The getter formats the JSON string using the map format.

Replace the names and appellations lists with these static, empty lists.

class PirateName {
  static final Random indexGen = new Random();

  static List<String> names = [];
  static List<String> appellations = [];
  ...
}
  • Be sure to remove final from these declarations.

  • [] is equivalent to new List().

  • A List is a generic type—a List can contain any kind of object. If you intend for a list to contain only strings, you can declare it as List<String>.


Add two static functions to the PirateName class.

class PirateName {
  ...

  static Future readyThePirates() async {
    String path =
        'https://www.dartlang.org/f/piratenames.json';
    String jsonString = await HttpRequest.getString(path);
    _parsePirateNamesFromJSON(jsonString);
  }

  static _parsePirateNamesFromJSON(String jsonString) {
    Map pirateNames = JSON.decode(jsonString);
    names = pirateNames['names'];
    appellations = pirateNames['appellations'];
  }
}
  • readyThePirates is marked with the async keyword. An async function returns a Future immediately, so the caller has the opportunity to do something else while waiting for the function to complete its work.

  • HttpRequest is a utility for retriving data from a URL.

  • getString() is a convenience method for doing a simple GET request that returns a string.

  • getString() is reading the piratenames.json file stored on dartlang.

  • getString() is asynchronous. It sets up the GET request and returns a Future that completes when the GET request is finished.

  • The await expression, which can only be used in an async function, causes execution to pause until the GET request is finished (when the Future returned by getString() completes).

  • After the GET request returns a JSON string, the code extracts pirate names and appellations from the string.


Add a top-level variable.

SpanElement badgeNameElement;

void main() {
  ...
}
  • Stash the span element for repeated use instead of querying the DOM for it.

Make these changes to the main() function.

void main() {
  InputElement inputField = querySelector('#inputName');
  inputField.onInput.listen(updateBadge);
  genButton = querySelector('#generateButton');
  genButton.onClick.listen(generateBadge);

  badgeNameElement = querySelector('#badgeName');
}
  • Stash the span element in the global variable. Also, stash the input element in a local variable.

Then, add the code to get the names from the JSON file, handling both success and failure.

main() async {
  ...

  try {
    await PirateName.readyThePirates();
    //on success
    inputField.disabled = false; //enable
    genButton.disabled = false; //enable
  } catch (arrr) {
    print('Error initializing pirate names: $arrr');
    badgeNameElement.text = 'Arrr! No names.';
  }
}
  • Mark the function body with async, so this function can use the await keyword.

  • Remove void as the return type for main(). Asynchronous functions must return a Future, so you can either specify a Future return type or leave it blank.

  • Call the readyThePirates() function, which immediately returns a Future.

  • When the Future returned by readyThePirates() successfully completes, set up the UI.

  • Use try and catch to detect and handle errors.

Edit the HTML code.

Disable the input field and the button.
...
  <div>
    <input type="text" id="inputName" maxlength="15" disabled>
  </div>
  <div>
    <button id="generateButton" disabled>Aye! Gimme a name!</button>
  </div>
...
  • The Dart code enables the text field and the button after the pirates names are successfully read from the JSON file.

Test it!

Click the Run button.

Type in the input field. Remove the text from the input field. Click the button.

Problems?

Check your code against the solution.

Or open the solution in DartPad.


What next?

Now that you’ve written your app, what do you do next? Here are some suggestions.

Continue to play with DartPad

DartPad includes a number of samples that you can play with, or you can create your own. Choose a sample using the Samples button in the upper right corner.

A screenshot of the samples menu in DartPad, including Clock, Fibonacci, and Sunflower

Try programming with an editor or IDE

Once you are ready to use a real editor or IDE, like WebStorm, read the Get Started tutorial for information on getting Dart, using the Dart tools, and the directory structure used by Dart apps.

If you want to continue to play with the pirate app, download the source. The code that corresponds to each step in this lab is under the darrrt directory.

Build your app to run in any browser

To compile the app into JavaScript that runs in any modern browser, use pub build. For example, to build the final version of the app:

cd darrrt/5-final
pub build

For more information, see Get Started and pub build, one of the pub commands.

Deploy a server and your app

The server side code lab allows you create a pirate crew by storing pirate names to a RESTful Dart server.

Also, see the Write HTTP Clients & Servers tutorial if you are interested in server-side programming.

Read the tutorials.

Learn more about Dart from the Dart tutorials.


Summary and resources

Think about what you’ve done!

This code lab provided a tour of most Dart language features and many library features. Here’s where to go to learn more.

The Dart language

A Tour of the Dart Language shows you how to use each major Dart feature, from variables and operators to classes and libraries. This code lab introduced the following Dart language features, all of which are covered in more detail in the language tour.

  • string interpolation ('$_firstName the $_appellation')
  • the cascade operator (..)
  • the fat arrow (=>) function syntax
  • the ternary operator (?:)
  • named constructors (PirateName.fromJSON(...))
  • optional parameters
  • a class
  • getters
  • instance methods and fields
  • class level methods and fields
  • top-level variables and functions
  • typecasting with as ((e.target as InputElement))
  • import, and import with show (import 'dart:math' show Random;)
  • generics
  • asynchrony support (async and await)
  • DartPad

Online documentation

The Dart libraries

API documentation for classes

API documentation for libraries

API documentation for the JSON constant

Feedback

Please provide feedback to the appropriate repo:

www.dartlang.org repo
For feedback about this code lab.
DartPad repo
For feedback about DartPad.