Friday, May 24, 2013

Rethinking DOM Menus in Dart

‹prev | My Chain | next›

My house is a mess and I am not quite sure how to fix it. The house in this case is one of the classes in the Dart version of the ICE Code Editor. This is not a Dart problem—I had the same situation in the JavaScript version. But the problem is more obvious in Dart.

Much of the effort of late in ICE has gone into the full-screen, lite-IDE that is used in 3D Game Programming for Kids. The class, Full starts off well enough:
class Full {
  Full() { /* ... */ }
  Future get editorReady => _ice.editorReady;
  String get content => _ice.content;
  void set content(data) => _ice.content = data;
  // ...
}
(the Full() method is the constructor)

As we have been building out the various menus and dialogs that go into an IDE, things have gone a little wrong. The menus and dialogs are clearly private methods—no external class should ever need to open the full-screen share dialog. Since Dart has first-class support for private methods, I have been dutifully marking these methods as private. It turns out that there are a lot of them:
class Full {
  // ...
  _attachToolbar() { /* ... */ }
  _attachMainMenuButton(parent) { /* ... */ }
  _attachKeyboardHandlers() { /* ... */ }
  _attachMouseHandlers() { /* ... */ }
  _showMainMenu() { /* ... */ }
  _hideMenu() { /* ... */ }
  _hideDialog() { /* ... */ }

  get _newProjectMenuItem { /* ... */ }
  _openNewProjectDialog() { /* ... */ }
  _saveNewProject() { /* ... */ }

  get _projectsMenuItem { /* ... */ }
  _openProjectsMenu() { /* ... */ }
  _openProject(title) { /* ... */ }

  get _saveMenuItem { /* ... */ }
  void _save() { /* ... */ }

  get _shareMenuItem { /* ... */ }
  _openShareDialog() { /* ... */ }
}
I am not quite certain how best to reduce the noise. The most obvious thing—the thing that I have been threatening since this was in JavaScript—is to move dialogs and their associated actions into self-contained classes. The biggest unknown for me is how to allow communication back into the main class.

The dialog that needs the most access is the Projects dialog, whose current entry point is _projectsMenutItem(). In addition to doing menu-like things, it needs to read from and write to the localStorage Store class. It also needs to be able to update the code editor when switching between projects. Since that seems the hardest, I start with it.

The entry point is still the menu list item that goes on the main menu. This will have to be exposed as a getter, which I call el:
class ProjectsDialog {
  // ...
  Element get el {
    return new Element.html('<li>Projects</li>')
      ..onClick.listen((e)=> _hideMenu())
      ..onClick.listen((e)=> _openProjectsMenu());
  }
  // ...
}
I pull in the _openProjectsMenu() method without change, along with the _openProject() method. Already the cohesion of these methods is improved. To continue to work, they need access to the parent element of the Full editor, as well as the editor itself and the data store. So I define a constructor that accepts all three:
class ProjectsDialog {
  var parent, ice, store;
  ProjectsDialog(this.parent, this.ice, this.store);
  Element get el {
    return new Element.html('<li>Projects</li>')
      ..onClick.listen((e)=> _hideMenu())
      ..onClick.listen((e)=> _openProjectsMenu());
  }
  // ...
}
With that, I can return the main menu, which is still in the Full class, and inject the new ProjectsDialog as:
class Full {
  // ...
  _showMainMenu() {
    var menu = new Element.html('<ul class=ice-menu>');
    el.children.add(menu);

    menu.children
      ..add(new ProjectsDialog(el, _ice, _store).el)
      ..add(_newProjectMenuItem)
      ..add(new Element.html('<li>Rename</li>'))
      ..add(new Element.html('<li>Make a Copy</li>'))
      ..add(_saveMenuItem)
      ..add(_shareMenuItem)
      ..add(new Element.html('<li>Download</li>'))
      ..add(new Element.html('<li>Help</li>'));
  }
  // ...
}
And that works. All of my tests still pass. I even give it a try in the sample app and it still works.

It seems a bit ugly to pass in those three parameters to the ProjectsDialog constructor. If only there was something that encapsulated those three objects… like the Full object that is creating them maybe?

Then the menu list could start as:
    menu.children
      ..add(new ProjectsDialog(this).el)
      ..add(_newProjectMenuItem)
      ..add(new Element.html('<li>Rename</li>'))
      // ...
That works, but the _ice and _store properties can no longer be private. I am not sure that is worth the change, so I call it a night here to give the idea a chance to percolate.

Regardless, I like the overall approach to extracting the menu dialogs out of the Full IDE class. This approach definitely has promise. And having a strong test suite in place to guard against regressions is invaluable.

Day #761

No comments:

Post a Comment