Sunday, September 22, 2013

Generic GUI Library in Dart (Part 2)

In the Part 1 of this post, I used Polymer to create the web page, or to construct the page DOM object.(here is Part3)
In this revised version(generic-gui-v1), I removed such dependency to Polymer, and directly created DOM in Dart classes.



The major reason for doing this was to support component style GUI development.
In Polymer, or some other template mechanism based on some extension of HTML, the logic associated with such GUI element and actual code which would be implemented in a Dart class are separated, and basically things are getting messy.

Component will keep such DOM creation logic in itself. therefore we can construct a new component using the DOM associated with the sub components. This is the power of integrating them rather than delegating to a separate template files.

Another idea introduced here is automatic component management.
when components are created, implicitly corresponding tree node structure is created. This will provide better Query feature against components.
Although DOM can be found by Dart's query method for DOM, but we cannot identify a component associated with the DOM node. So this should be very useful.
On the other hand, if we have a component, we can have the corresponding DOM node immediately.

Following Component and TopComponent classes were introduced to support such abstract component structures.
The Component class will be used as super class of all UI component class like Table, Row etc.
It contains DOM element and its sub components.

The management of these structure are almost automatic, so each component class need not have to aware of these underlying structure building.

Fine point is the order of component creation and Element creation. To create a component, the parent component must be created first, but in order to create a DOM element, we need to create from the children.

So this means we cannot create element inside of component constructor.
But this will be taken care by init method in TopComponent. Namely this class is top level component, and only for this object, init is supported. The constructor of the subclass of TopComponent should invoke init in the end of the constructor.

 /*
 * Component should be created from top down, while DOM node should be created from bottom up
 */

abstract class Component {
  static int _gid = 0;
 
  final int id = _gid++;
  final List<String> classes;
  Element _element;
 
  final Component parent;
  final List<Component> children = [];
 
  Component(Component this.parent, List<String> this.classes) {
    if (parent != null) parent.children.add(this);
  }
 
  // DOM
  Element get element => (_element == null)?_element = _createElement():_element;
   
  // invoked from top level node.
  void _init() {
    children.forEach((Component child)=>child._init());
    _element = _createElement();
  }

  Element _createElement() =>
    createElement()
      ..id = "id-${id}"
      ..classes.addAll(classes);

  // this should not be invoked execpt from _init!
  Element createElement();
 
  //
  // utils
  //

  TopComponent topComponent() =>  (parent == null)?(this as TopComponent):parent.topComponent();
 
  Component findById(int id0) {
    if (id0 == id) {
      return this;
    }
    for (Component c in children) {
      Component c0 = c.findById(id0);
      if (c0 != null) {
        return c0;
      }
    }
    return null;
  }
 
  dynamic fold(var initialValue, dynamic combine(var previousValue, Component child)) =>
    children.fold(combine(initialValue, this), (pr, child) => child.fold(pr, combine));

}

abstract class TopComponent extends Component {
  static const String TOP_NODE = "g_top";
 
  TopComponent(): super(null, [TOP_NODE]);
 
  void init() {
    _init();
  }
}
The Table classes have been revised as followings:

class HeaderCell extends Component {
  static const String HCELL = "g_hcell";
  TableHeader tableHeader;
  Symbol symbol;
  Type type;
 
  // GUI
  String label;
 
  HeaderCell(TableHeader parent, this.symbol, this.type, this.label, {List<String> classes: const [HCELL]}): super(parent, classes), this.tableHeader = parent;
 
  factory HeaderCell.fromSymbol(TableHeader tableHeader, Symbol symbol, Type type) =>
    new HeaderCell(tableHeader, symbol, type, MirrorSystem.getName(symbol));
 
  RowCell defaultRowCell(Row row) => new RowCell(row, this);
 
  // DOM
  Element createElement() => new Element.td()..text = label;
}

class RowCell extends Component {
  static const String RCELL = "g_rcell";
  HeaderCell headerCell;
  Object value = null;
 
  RowCell(Row parent, this.headerCell, {Object init, List<String> classes : const [RCELL]}): super(parent, classes) {
    value = init;
  }
 
  // DOM
  Element createElement() => new Element.td()..text = (value == null)?'':value.toString();
}

class TableHeader<E> extends Component {
  static const String TH = "g_th";
  final Table<E> table;
  final List<HeaderCell> headerCells;
 
  TableHeader(Table parent, this.headerCells, {List<String> classes: const [TH]}): super(parent, classes), table = parent;
 
  factory TableHeader.fromType(Table parent, Type modelType) {
    var th = new TableHeader(parent, []);
    reflectClass(modelType).getters.forEach((Symbol symbol, MethodMirror md){
      //print(">> symbol: ${symbol}, returnType: ${(md.returnType as ClassMirror).reflectedType}");
      th.headerCells.add(new HeaderCell.fromSymbol(th, symbol, (md.returnType as ClassMirror).reflectedType));
    });
    return th;
  }
 
  // DOM
  Element createElement() => headerCells.fold(new Element.tag("thead"), (Element thead, HeaderCell hc)=>thead..nodes.add(hc.element));
}

class Row<E> extends Component {
  static const String TR = "g_tr";
  final Table<E> table;
  final E e;
  final List<RowCell> rowCells = [];

  Row(Table parent, this.e, {List<String> classes: const [TR]}): super(parent, classes), table = parent;
   
  factory Row.defaultRow(Table<E> _table) =>
    table.theader.headerCells.fold(new Row(table, null), (Row row, HeaderCell hc) => row..rowCells.add(hc.defaultRowCell(row)));
 
  factory Row.fromEntity(Table<E> table, E e) {
    InstanceMirror imirr = reflect(e);
    return table.theader.headerCells.fold(new Row(table, e), (Row row, HeaderCell hc) => row..rowCells.add(new RowCell(row, hc, init: imirr.getField(hc.symbol).reflectee)));
  }
 
  // DOM
  TableRowElement createElement() => rowCells.fold(new Element.tag("tr"), (TableRowElement row, RowCell rc)=>row..nodes.add(rc.element));
}

class Table<E> extends Component {
  static const String TABLE = "g_table";
  Type modelType;
  String name;
  TableHeader theader;
  final List<Row<E>> rows;
 
  Table(Component parent, this.modelType, {List<String>  classes: const [TABLE]}): super(parent, classes), rows = [];
 
  factory Table.fromModelType(Component parent, Type modelType) =>
    [new Table(parent, modelType)].fold(null, (p, tbl)=>tbl..theader = new TableHeader.fromType(tbl, modelType)); 
 
  //
  void newRow() => rows.add(new Row<E>.defaultRow(this));
 
  void addRow(Row<E> row) {
    // check row.tabel == this
    if (row.table != this) {
      print(">> addRow error");
      throw new Exception("Table<1>");
    }
    rows.add(row);
    element.nodes.add(row.element);
  }
 
  void addRowFromEntity(E e) => addRow(new Row<E>.fromEntity(this, e));
 
  void clear() {
    element.nodes.clear();
    element.nodes.add(theader.element);
    rows.clear();
  }
 
  void load(Iterable<E> es) {
    clear();
    es.forEach(addRowFromEntity);
  }
 
  // DOM
  Element createElement() => rows.fold(new Element.tag("table"), (Element tab, Row row)=>tab..nodes.add(row.element));
}
This is the sample application using these library.
 class AppController extends TopComponent {
  final DivElement _uiRoot;
  DivElement _content;
  DivElement _actions;
 
  Table<Expense> _table;
 
  AppController(this._uiRoot, {List<Expense> expenses}) {
    _table = new Table<Expense>.fromModelType(this, Expense);
    if (expenses != null) {
      _table.load(expenses);
    }
    init();
    _uiRoot.nodes.add(element);
  }

  Element createElement() =>
    new Element.tag("div")
      ..classes.add("section")
      ..nodes.add(new Element.html("<header class='section'>Generic Table</header>"))
      ..nodes.add(_content = new Element.tag("div")
        ..nodes.add(_table.element))
      ..nodes.add(_actions = new Element.tag("div")
        ..id = "actions"
        ..classes.add("section"))
      ..nodes.add(new Element.html("<footer class='section' id='footer'></footer>"));
 
  Table<Expense> get table => _table;
}

main() {
  List<Expense> expenses = [
                            new Expense.random(),
                            new Expense.random(),
                            new Expense.random(),
                            new Expense.random(),
                            new Expense.random()];
  Element uiContainer = document.query("#generic_table");
  AppController app = new AppController(uiContainer);
  app.table.load(expenses);

}
 The html file is very simple.
<html>
  <head>
    <meta charset="utf-8">
    <title>Sample app</title>
    <link rel="stylesheet" href="generic_table.css">
  </head>
  <body>
    <div id="generic_table"></div>
    <script type="application/dart" src="generic_table.dart"></script>
    <script src="packages/browser/dart.js"></script>
  </body>
</html>


No comments:

Post a Comment