Saturday, October 19, 2013

A Classical Controller in Angular.dart


Tonight, I continue my exploration of Angular.dart, the Dart port of the execllent AngularJS. I am starting to develop a feel for the differences between the original JavaScript and Dart versions. So far the differences have been something of an impedance mismatch, where the Dart version felt clunky compared with what I am used to in AngularJS (not that I've done a ton of AngularJS).

I think Angular.dart made a pretty solid first attempt to mimic the AngularJS controller structure. Ultimately, the difference between prototypical and classical inheritance was probably a bit too much. JavaScript's ability to access object literals with the dot operator whereas Dart maps can only use square brackets further weakens the attempt.

I don't think that I am revealing any big secrets here. The Angular.dart folks figured this out long before I started looking at the project. Indeed, they have already started on a new and improved approach that plays better to Dart's strengths. So tonight, I am going to convert my current AngularJS-like approach:
class AppointmentCtrl {
  AppointmentCtrl(Scope scope) {
    scope
      ..['appointments'] = [{'time': '08:00', 'title': 'Wake Up'}]
      ..['addAppointment'] = (){
            var newAppt = fromText(scope['appointmentText']);
            scope['appointments'].add(newAppt);
            scope['appointmentText'] = null;
          };
  }
  // ...
}
In the current approach, nearly all of the work is done in the constructor, which requires an injection “scope.” Even in AngularJS, I am a little uncomfortable with $scope, which seems to attract cruft (though the hierarchy built into Angular ensures this isn't too bad). Regardless, it just looks ugly doing all that work in a constructor.

It turns out that constructors are not needed at all. Instead, I can convert the list of appointments to an instance variable and the addAppointment function into a method, which I renamed as the more compact add():
class AppointmentCtrl {
  List appointments = [{'time': '08:00', 'title': 'Wake Up'}];
  String newAppointmentText;

  void add() {
    var newAppt = fromText(newAppointmentText);
    appointments.add(newAppt);
    newAppointmentText = null;
  }
  // ...
}
That is much clearer and reads like any other Dart class. I like it!

The other change that I need to make inside of the class is to expose a text value to mirror the ng-model element of the same name in the HTML. Here, I am using newAppointmentText:
class AppointmentCtrl {
  List appointments = [{'time': '08:00', 'title': 'Wake Up'}];
  String newAppointmentText;

  void add() {
    var newAppt = fromText(newAppointmentText);
    appointments.add(newAppt);
    newAppointmentText = null;
  }
  // ...
}
The add() method then uses this value to get input from the HTML counterpart, convert it into an appointment data structure, and add the result to the list of appointments. After accepting input, the add() completes its work by resetting the ng-model attribute back to null so that the user can add another appointment from the UI.

There is one other bit of work required in the backing controller class. I need to add a publishAs attribute to the @NgDirective above the class:
@NgDirective(
  selector: '[appt-controller]',
  publishAs: 'day'
)
class AppointmentCtrl {
  // ...
}
The selector attribute of the annotation tells Angular.dart how to identify the containing HTML element (e.g. <div appt-controller>). The publishAs attribute tells Angular.dart how it can refer to the controller instance. For every element with the declared selector, any child elements can now refer to the associated AppointmetnCtrl instance with the day identifier.

Thus, my HTML becomes:
    <div appt-controller>
      <ul class="unstyled">
        <li ng-repeat="appt in day.appointments">
          {{appt.time}} {{appt.title}}
        </li>
      </ul>
      <form onsubmit="return false;" class="form-inline">
        <input ng-model="day.newAppointmentText" type="text" size="30"
               placeholder="15:00 Learn Dart">
        <input ng-click="day.add()" class="btn-primary" type="submit" value="add">
      </form>
    </div>
Now the ng-repeat iterates over day.appointments, the ng-click invokes the day.add() method, and the ng-model refers to the day.newAppointmentText property. Thanks to the publishAs, each of these is exactly like accessing a property or invoking a method on an instance of AppointmentCtrl named day. There still a bit of magic at work here—the day instance is “just there” by virtue of appt-controller—but I do prefer an object being automatically created than a bunch of functions and variables being around. It seems more real, somehow.

Coming from a AngularJS background, I found the barrier to entry lowest with the previous approach that mimicked AngularJS controllers by injecting scope into the constructor. But I quickly found it off-putting and awkward. This class-based approach feels like home.


Day #909

4 comments:

  1. "Indeed, they have already started on a new and improved approach that plays better to Dart's strengths."

    What project are you referring to?

    ReplyDelete
    Replies
    1. Heh. I think it's best to just ignore that comment :)

      FWIW, I meant the Dart port of Angular. It's my understanding that that Dart and JavaScript ports will converge at some point in the near future, so that could also have been taken to mean the Angular project in general, but I really meant just the Dart port.

      Even though this post is only 1 month old, the developers on that project have done an _astonishing_ amount of work on Angular.dart. When I was just picking it up here, I found that I could still code Angular.dart in a very AngularJS kinda way. It was, at the same time, both comfortable and confusing. If you click the “next >” link in the article, you'll see that I explored the newer, more Dart-like API in subsequent days (it's beautiful).

      But really, it's safest just to ignore that comment as a bit too much stream of consciousness leaking into the post :)

      Delete
  2. Hey Chris, thank you very much for the speedy reply. Glad you like the dart-port since I was just about to get my feet wet with that. Have to admit I have no experience with javascript MVC's, so it will be quite a leap.

    On the Angular-dart side, I noticed that quite some JS files still exist within the repository. Also I believe that support for forms is not yet implemented.

    Things do seem to move fast though.

    ReplyDelete
    Replies
    1. Sounds like fun! Hopefully my ramblings are not too out-of-date. Even if they are, the Angular.dart folk were putting together some pretty slamming documentation and tutorials. Also, they're super helpful with questions. It'll be a challenge, but hopefully a fun one.

      I would imagine the JS files are just there as reference. I don't recall any actual code going through JavaScript. Form support is a nice-to-have, but as I noted here, it is easy to workaround. They can't get everything done right away, so they've made some very pragmatic choices on what to start with.

      And with the speed at which they're moving, they'll probably finished forms while we were talking :D

      Delete