Extending the to-do list

We can keep index.html as is and start by adding routing to our main.dart file:

// web/main.dart
moduleRouteInitializer(Router router, RouteViewFactory views) {
  views.configure({
    // This route will be displayed even when no path is set.
    'add': ngRoute(
        defaultRoute: true,
        path: '/add',
        view: 'view/add.html'),
    // Detail of a task identified by its id.
    'detail': ngRoute(
        path: '/detail/:taskId',
        view: 'view/detail.html'),
  });
}

class MyAppModule extends Module {
  MyAppModule() {
    bind(TodoListComponent);
    bind(TodoDetailComponent);
    // Initialize routes.
    bind(RouteInitializerFn, toValue: moduleRouteInitializer);
    // Turn on listening to Window.onHashChange event.
    bind(NgRoutingUsePushState,
        toValue: new NgRoutingUsePushState.value(false));
  }
}

In this case, route paths are paths after the hash sign in your URL. That's, for example, index.html#/add or index.html#/detail/3. However, you could tell AngularDart to match the entire URL and not just the hash part by setting the following:

bind(NgRoutingUsePushState,
    toValue: new NgRoutingUsePushState.value(true));

But this isn't very common.

Routes can be also nested:

'detail': ngRoute(
    path: '/detail/:taskId',
    mount: {
      'edit': ngRoute(
          path: '/edit',
          view: 'view/edit.html'),
    }
)

This would match routes such as /detail/3/edit, but we're not going to use it here in order to keep the example simple.

Model

Instead of keeping tasks as a list of strings like we did in the preceding section, we'll turn it into a class called Task:

// lib/service/task.dart
class Task {
  int id;
  String title;
  String when;
  
  Task(this.id, this.title, this.when);
}

This is pretty simple, just three public properties and a constructor. We'll see that we can print object properties straight into the template.

Then, for practical reasons, we'll move default tasks into a JSON file in web/default_tasks.json:

[{"id":1,"title":"Buy more cat food","when":"12:00"},
{"id":2,"title":"Feed the cat","when":"11:00"},
{"id":3,"title":"Do the laundry","when":"17:15"}]

View

Now, create two HTML templates in the web/view directory. First, we'll create add.html:

<!-- web/view/add.html -->
<h2>Add a new task</h2>
<p>when: <input type="time" ng-model="newTask['when']"></p>
<p>title: <input type="text" ng-model="newTask['title']"></p>
<button type="button"  ng-click="addTask(newTask['when'], newTask['title'])">Add
</button>

Now, let's move on to creating detail.html:

<!-- web/view/detail.html -->
<h2>detail</h2>
<todo-detail all-tasks="tasks"></todo-detail>
<a href="#/add">add task</a>

These two HTML files represent templates for routes that a user can navigate to. The content of one of these templates will be inserted by Angular Dart into the <ng-view> tag according to the current matching route. We'll explain Angular directives used in these templates in a moment.

Component

Let's see the first part of our updated TodoListComponent class:

// lib/component/todo_list.dart
// Mark class as Component with an annotation.
@Component(
    selector: 'todo-list', // CSS selector for this component
    templateUrl: 'todo_list.html',
    exportExpressions: const ['newTaskParams', 'addTask']
)
class TodoListComponent {
  // Parameters for a new task.
  Map<String, dynamic> newTask = {};
  String title = "My Todo list";
  // Keep all my tasks in a list.
  List<Task> tasks = [];
  // Term for quick search among my tasks.
  String search = '';
  
  TodoListComponent() {
    // Load default tasks. We can't use async/await here because
    // class constructor can't return a Future object and
    // therefore can't be declared with async keyword.
    HttpRequest.getString('/default_tasks.json').then((response) {
      List<Map<String, String>> decoded = JSON.decode(response);
      
      decoded.forEach((Map<dynamic, String> taskData) {
        // Force integer value.
        int id = int.parse(data['id'].toString());
        // Append a new task to the list.
        tasks.add(new Task(id, data['title'], data['when']));
      });
    });
  }
  /* … */
}

You can see that we switched from template to templateUrl. As this template is going to be in the same directory as todo_list.dart, which isn't accessible by a browser, we need to tell AngularDart where to find it. Locate the following in pubspec.yaml:

transformers:
- angular

Replace the preceding lines with:

transformers:
- angular:
    html_files:
      - lib/component/todo_list.html
      - lib/component/todo_detail.html

Also, we used a new exportExpressions option. This tells AngularDart about expressions that aren't discoverable statically. This might be quite confusing at first sign. When you run an app using AngularDart, it automatically looks for expressions used in your code and generates a map of all getters in the main_static_expressions.dart file. This file is imported right at the top of main.dart when you publish your application with Pub Build (or run it in Dartium from Dart Editor).

We use both the addTask() method and the newTaskParams public property, but inside the add.html template, which is loaded dynamically and is therefore not found by AngularDart when generating main_static_expressions.dart. If you forgot to set expressions manually with exportExpressions, Dart will throw an error:

Missing getter: (o) => o.myLostProperty

You'll probably see this type of error a few times when using AngularDart, so if you're not sure whether all the getters that you use are found statically, you can take a look at main_static_expressions.dart and check what it found for you.

Component

We see that the first two expressions are those that we defined in exportExpressions.

Next, we loaded the default tasks in the constructor and filled the tasks list with instances of the Task class.

Finishing TodoListComponent

Now we can finish TodoListComponent with methods to add and remove tasks:

class TodoListComponent {
  /* … */

  void addTask(DateTime when, String title) {
    // Convert DateTime to HH:mm format.
    String str = "${when.hour.toString().padLeft(2, '0')}:"
               + "${when.minute.toString().padLeft(2, '0')}";
    
    // Find max id among all current tasks.
    int maxId = tasks.length == 0
        ? 1 : tasks.map((elm) => elm.id).toList().reduce(max);
    
    // Create a new instance of Task and append it to the list.
    tasks.add(new Task(maxId + 1, title, str));
    
    newTask = {};
  }
  
  // Remove task by id.
  void removeTask(MouseEvent e, int id) {
    e.preventDefault();
    tasks.removeWhere((task) => task.id == id);
  }
}

We can already see how we create new tasks with two inputs and a button in add.html. Let's recap the input fields and buttons here with emphasized ng-* directives:

<input type="time" ng-model="newTask['when']">
<input type="text" ng-model="newTask['title']">
<button type="button"
    ng-click="addTask(newTask['when'], newTask['title'])">
Add</button>

Every change in inputs is propagated to the newTask map (that's the ng-model directive). Note that newTask is an empty map at the beginning.

Then, when you click on the button, we call the addTask() method and pass both arguments right from the template. The addTask() method creates a new task and sets newTask = {} at the end, which clears both inputs (two-way data binding).

Now, we'll create the template for our TodoListComponent in todo_list.html:

<!-- lib/component/todo_list.html -->
<h1>{{ title }}</h1>
<ng-view></ng-view>
<h2>My tasks</h2>
Search: <input type="search" ng-model="search">

<ul>
<li ng-repeat="t in tasks|filter:{'title':search}|orderBy:'when'">
  {{ t.when }} - <a href="#/detail/{{ t.id }}">{{ t.title }}</a>
  <a href="" ng-click="removeTask($event, t.id)">[remove]</a>
</li>
</ul>
<p ng-if="tasks.length == 0">Congratulations!</p>

We already mentioned that the template for the current matching route is placed in <ng-view>. On a page load, it's going to be add.html because it's marked as default. We tied only a small part of the application to routes, but you can change the entire layout with <ng-view> as well.

The ng-repeat directive is extended with two formatters. A formatter is basically a function that takes input data, processes it, and returns it. In Angular templates, formatters can be chained with the | character, and always take the result of the expression on their left-hand side as the input.

Our example uses two formatters:

  • filter: This passes only items that match the defined criteria. You can use just a simple expression and the filter formatter will check all items' properties and if any of them contain the searched term, it will be passed to the output (for example, filter:'the'). We can also tell the filter to search in just one property by passing a map as an argument (in our example, we're searching only in task titles). The input list remains unchanged.
  • orderBy: This sorts the input list by a property. The input list remains unchanged.

The ng-repeat directive creates a new scope for each clone, which means that each <li> tag has its own instance of t representing a task.

Note that we set the href attribute of <a> to #/detail/id. Changing the URL's hash doesn't load a new page but it's fetched by AngularDart's router, and the route that matches this URL is evaluated (for us, the content of <ng-view> is replaced by an appropriate template).

The ng-click="removeTask($event, t.id)" directive is the same as addTask() but uses a special $event variable provided automatically by AngularDart. We need to use it because by default, <a> changes a browser's location to what's in the href attribute. But that's not what we want, so we call preventDefault() on the event to prevent the default browser behavior (that's the first line of the removeTask() method).

At the end of todo_list.html, we have another ng-if directive, which causes itself and its subtree to be visible only when the expression is evaluated to true. There are actually two methods for showing/hiding elements in Angular:

  • ng-if: When the expression is evaluated as false, it removes the entire subtree from the DOM. When it's true, it has to create the entire tree again. This directive also creates a new scope.
  • ng-show: When the expression is evaluated as false, the DOM subtree is just hidden but still exists in the DOM. This directive doesn't create a new scope.

In many cases, these two are interchangeable. Just if you know you don't need new scopes, or the subtree is relatively small and is showed/hidden many times during the page's lifetime, it's probably better to use ng-show.

The last thing is the task's detail, which is a component declared in todo_detail.dart. We already saw in detail.html where it's going to be placed:

// lib/component/todo_detail.dart
// Usage: <todo-detail all-tasks="tasks"></todo-detail>
@Component(
    selector: 'todo-detail', // CSS selector for this component.
    templateUrl: 'todo_detail.html'
)
class TodoDetailComponent {
  // One way data binding, this component needs a reference
  // to all tasks but won't modify it internally.
  // Note that this property has to be public.
  @NgOneWay('all-tasks')
  List<Task> allTasks = null;
  
  // Task id from route parameters.
  int _id;

  Task _task = null;
  
  // Constructor that takes route parameters.
  TodoDetailComponent(RouteProvider routeProvider) {
    _id = int.parse(routeProvider.parameters['taskId']);
  }

  // Custom getter to avoid unnecessary iterations of the list.
  Task get task {
    // We need to check if allTasks isn't null.
    if (_task == null && allTasks != null) {
      allTasks.forEach((Task t) {
        if (t.id == _id) _task = t;
      });
    }
    return _task;
  }
}

This component is instantiated by AngularDart every time the URL matches the route with the detail.html template because it contains the <todo-detail> element.

We annotated the List<Task> allTasks property with @NgOneWay('all-tasks'), which tells AngularDart to put the result of an expression in the element's all-tasks attribute into the allTasks property. With this, we connected TodoListComponent and TodoDetailComponent components together.

Data bindings

There are three different annotations to bind attributes to object properties:

  • NgOneWay: This is one way-data binding. The expression result from the attribute is passed to the object. Any change in the expression variable reevaluates the entire expression and a new value is passed to the object's property. However, changing the object's property doesn't propagate back to the expression's variable. Therefore, this is one-way (or unidirectional) binding.

    Note that when passing entire objects, AngularDart doesn't make copies of them. It passes just their references, so changing its properties actually changes the original object. For example, if we set _task.title = "hello" in the task getter, it would change its title in TodoListComponent as well. But setting allTasks = [] will just create a new list and assign its reference to the allTasks property. The original reference passed from the attribute's expression remains unchanged.

  • NgTwoWay: This is two-way data binding. Changing the property value will be propagated back to the attribute's variable. Also, changing the attribute variable will change the value of the tied property.
  • NgAttr: This is another unidirectional connection similar to NgOneWay but the attribute's values is passed as is, although you can use interpolation with "mustaches" {{ }}.

The difference between NgOneWay and NgAttr is that if we used NgAttr in TodoDetailComponent, AngularDart would take the attribute's "tasks" value as a string and wouldn't evaluate it as an expression. Therefore, this would throw an error because we can't assign a string to a variable of type List.

As TodoDetailComponent is instantiated every time we create the <todo-detail> element, we can define a constructor that accepts a RouteProvider object with all route parameters. For our purpose, this is the task's ID.

We can't rely on the order of variable bindings, and it's possible that the task getter in TodoDetailComponent will be called before the allTasks property is set. For this reason, we need to check whether allTasks was already set inside the task getter before we try to iterate it.

The last template is todo_detail.html, which is very simple:

<ul>
  <li>id: {{ task.id }}</li>
  <li>when: {{ task.when }}</li>
  <li>title: {{ task.title }}</li>
</ul>

The directory structure with all the files should look like this.

Data bindings

Finally, we can run our application in the browser.

Data bindings

You can try that any change is immediately propagated to the view and things such as filtering the task list by the title (writing into the search input field) work and we didn't have to even touch the DOM by ourselves.

Don't worry if all this seems complicated to you, especially if you've never used any Angular before. We encourage you to download the source code for this example and play with it or take a look at the official tutorials at https://angulardart.org/tutorial/.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.163.180