Saturday, October 26, 2013

Routing Without Scope in Angular.dart


Scope is the devil's plaything.

Scope is defined as the range on one's perception. In programming, scope is the context in which an identifier is available. In the real world, it is generally considered a good thing to have broad scope—the wider a perspective, the more informed a decision. More information in programming scope is bad. The more identifiers that can influence a particular decision in code, the greater the chance that an unrelated change somewhere else can introduce bugs. “Sure I updated the params in that class, it fixed a bug… Oh, the same params object is used over there for that case? Ugh.”

So the less information available to code, the better. This is one of the things that makes AngularJS so appealing. By default, nothing is available to the various templates, components, controllers, and routes. Developers have to explicitly inject objects that are needed for each. Each of the objects that can be injected is responsible for just one thing (route parameters, server interaction, form control). Between dependency injection and single responsibility objects, AngularJS tends toward exceptionally clean code. Except for scope.

Scope in AngularJS is mostly intended to sync controller variables for use in views. But it is too easy to become a dumping ground for things that ought to get their own containing classes. This is one of the reasons that I really like the Angular.dart approach to controllers, which use instance properties rather than scope.

And yet, scope still exists in Angular.dart. And, to my shame, I dumped stuff in there last night. Unable to figure out the right way to make route information available to my controllers, I added it to scope:
class CalendarRouter implements RouteInitializer {
  Scope _scope;
  CalendarRouter(this._scope);

  void init(Router router, ViewFactory view) {
    router.root
      // ...
      ..addRoute(
          name: 'day-view',
          path: '/days/:dayId',
          enter: (RouteEvent e) {
            e.route.parameters.forEach((k,v) { _scope[k] = v; });
            return view('partials/day_view.html')(e);
          }
        );
  }
}
So now anything in my application has access to these parameters via scope. And if I ever make a change, anything in my application that relies on scope can break.

Fortunately, thanks to Artem Andreev, I think I understand the way Angular.dart want this done. Taking look back at what did not work yesterday, I was attempting to directly inject a RouteProvider into my controller:
@NgDirective(
  selector: '[day-view-controller]',
  publishAs: 'appt'
)
class DayViewController {
  String id;
  String title = 'Default Title';
  String time = '08:00';
  AppointmentBackend _server;

  DayViewController(RouteProvider router, this._server) {
    id = router.route.parameters["dayId"];
    _server.
      get(id).
      then((rec) {
        title = rec['title'];
        time = rec['time'];
      });
  }
}
As both Artem and the documentation pointed out, if an attempt is made to inject a RouteProvider without a ng-bind-route directive in the HTML, the RouteProvider would always be null.

The day-view route specifies that the partials/day_view.html view be used, so I wrap the content inside an ng-bind-route directive:
<div ng-bind-route="day-view">
<div appt-controller>
  <!-- normal controller template stuff here -->
</div>
</div>
I do not understand the need for the ng-bind-route here. The routing has already bound the route to the view, this reciprocation seems an awful lot like duplication. Hopefully someday only one declaration of the relationship will be needed.

With that in place, I am back to where I spent much of yesterday: obscure dependency injection land. Specifically, I am getting:
NoSuchMethodError : method not found: 'simpleName'
Receiver: null
Arguments: []
There is also a 131 line stack trace which I choose to omit here for sanity's sake.

It turns out that last piece of the puzzle that I have been missing was a simple import. Most of the calendar related code resides in calendar.dart, which is separate from the Angular module and routing code in main.dart. And I simply forgot to import Angular routing into calendar.dart as I had already done in main.dart. With the fix in place:
import 'package:angular/angular.dart';
import 'package:angular/routing/module.dart';
import 'dart:convert';
@NgDirective(
  selector: '[day-view-controller]',
  publishAs: 'appt'
)
class DayViewController {
  // ...
}
I now have routing parameters working. The dayId is sent in via the RouteProvider, which I can use to retrieve the appointment information from the server, which then populates the template:



That is not quite the end of routing in Angular.dart. It seems the RouteHandle returned from router.route does a fair bit of scope work itself. So much so that it will stick around even if the route is no longer active. To get rid of it, I have to call its discard method. The time to discard a route is when a “detach aware” object is detached.

In other words, my controller needs to implement NgDetachAware and define a detach() method to discard the route:
@NgDirective(
  selector: '[day-view-controller]',
  publishAs: 'appt'
)
class DayViewController implements NgDetachAware {
  AppointmentBackend _server;
  RouteHandle route;

  DayViewController(RouteProvider router, this._server) {
    route = router.route;
    // ...
  }

  detach() {
    // The route handle must be discarded.
    route.discard();
  }
}
A class that implements NgDetachAware will have its detach() method invoked whenever it is no longer active—the perfect time to discard a route handle.

I think that covers the basics of routing in Angular.dart. There are still other topics (pushState, nested routes) that I may or may not investigate deeper, but I tend to expect that most will be fairly straight-forward now that I understand this. I am still skeptical about the need to to reciprocate the route-view binding. Given the ceremony involved with NgDetachAware, I wonder if my scope-based approach might have some merit after all. All the more so if RouteHandle has its own scope baggage.

But I will leave those questions for another day. For now, I am finally glad to have solved the puzzle of properly injecting routes into controllers.


Day #916

1 comment:

  1. In 0.0.8 RouteProvider changed to return Route instead of RouteHandle, so you don't need to do the route.discard() anymore. I also agree that having to use ng-bind-route when the framework could already figure out the route based on current view is a bit redundant and confusing. I think the API could be simplified here a little bit.

    ReplyDelete