Saturday, September 21, 2013

Generic GUI library in Dart (Part 1)

Most of Web application depend on  table view, and edit view.
Often these are related to database application.

These views are used to support CRUD operation.
The table view is essentially for query, to find a data.
The editor view is to create a new entity or edit existing entity.
Often these entities  reside in a remote database.

To achieve these thing, like Ruby on Rails, often code generation approach has been used. But this approach is not component oriented since generating code make it difficult to reuse codes

The better approach would be to develop generic GUI library.

The reason why this has not been used had several reasons coming from language short coming.

In order to support this approach, following language features are essential.
  • Class
  • Generics
  • Reflection
  • Annotation
  • direct DOM manipulation
Essentially, Java cannot  support direct DOM manipulation, and JavaScript doe not have all of class related feature as standard language feature.

So only Dart can support all of this.
In Java, it might be possible to use Pivot and running with Applet on browser.
But applet is not popular these days, and if we have more suitable language for web application, this approach is not so attractive. The Dart's concise syntax, and server side usage make it much simpler to develop web application than Java.

So I developed  first such application(generic-table-v1) in Dart.
Since this is rather primitive level of implementation, it will be easier to understand the basic of this approach.
In order to add more feature, it will be better not to rely on polymer.
These improvement will be the next step.

The following short code is all we need to have a class specific table view.
 main() {
  List<Expense> expenses = [
                            new Expense.random(),
                            new Expense.random(),
                            new Expense.random(),
                            new Expense.random(),
                            new Expense.random()];
  Table table = new Table.fromModelType(Expense)
    ..load(expenses)
    ..newRow()
    ;
 
  query('#tmpl').model = table;
}
and I used polymer to create corresponding template:
  <body>
    <h1>Generic table</h1>
    <template id="tmpl" bind>
      <table border="1"> 
        <tr>    
          <th template repeat="{{ theader.headerCells }}">
            {{label}}
          </th> 
        </tr>         
        <tr template repeat="{{ rows }}">
          <td template repeat="{{ rowCells }}"><input type="text" value="{{value}}"></td>
        </tr>
      </table>
    </template> 
    <script type="application/dart" src="generic_table.dart"></script>
  </body>
This is the shapshot of this table.


The point of this approach is if we have some entity class like Expense, we just need to pass this class to have different Table view.

A specific table view associated with an entity class is  implemented by the same generic class. So no code generation are required.
Even if we have 500 of entity tables, we still don't need to generate 500 of JSP files or something similar. just a single generic class can handle all of these cases.
This would reduce the build time as well as code size.

But the main advantage of this approach  is to allow developing component based GUI library.  If properly designed, subcomponents of table, like row, cell may be specialized using separate GUI libraries.
Reusing code will be as usual as normal software development.

This Table class was written to prove the feasibility of generic GUI approach in Dart,  but there are more potentials in this approach to enhance sophisticated interaction between GUI DOM node operations,  entities and GUI views. Also these can be combined with backend server as well.

 Since the code is still manageable size, I will show all the codes.
 library generic_gui;

import 'dart:mirrors';


class Cell {
}


class HeaderCell extends Cell {

  TableHeader tableHeader;
  String label;
  Type type;
  Symbol symbol;
 
  HeaderCell(this.tableHeader, this.symbol, this.type, this.label);
 
  factory HeaderCell.fromSymbol(TableHeader tableHeader, Symbol symbol, Type type) =>
    new HeaderCell(tableHeader, symbol, type,MirrorSystem.getName(symbol));
 
  RowCell defaultRowCell() => new RowCell(this);
}

class RowCell extends Cell {

  HeaderCell headerCell;
  Object value = null;
  RowCell(this.headerCell, {Object initv}) {
    if (initv != null) {
      value = initv;
    } else {
      // we may introduce  default value assignment depending on the type.
    }
  }
}

class TableHeader<E> {

  Table<E> table;
  List<HeaderCell> headerCells;
 
  TableHeader(this.headerCells);
 
  factory TableHeader.fromType(Type modelType) {
    var th = new TableHeader([]);
    reflectClass(modelType).getters.forEach((Symbol symbol, MethodMirror md){
      th.headerCells.add(new HeaderCell.fromSymbol(th, symbol, (md.returnType as ClassMirror).reflectedType));
    });
    return th;
  }
}

class Row<E> {

  final Table<E> _table;
  final E e;
  final List<RowCell> rowCells;

  Row(this._table, this.e, this.rowCells);

    
// I like this kind of single expression style. 
factory Row.defaultRow(Table<E> _table) => new Row(_table, null,

    _table.theader.headerCells.fold([], (rowCells, HeaderCell hcell)=>rowCells..add(hcell.defaultRowCell())));
// '..' syntax is perfect match with this fold function.
  // Sometimes, we need to declare variable, then we must use block statement, but if Dart supports FAMOUS 'let' syntax of ML(or F#), then this can be written as a single expression, s.t. ' => let var imirr = ... in ... end;'
  factory Row.fromEntity(Table<E> table, E e) {
    InstanceMirror imirr = reflect(e);
    return new Row(table, e, table.theader.headerCells.fold([], (cells, HeaderCell hc)=>
        cells..add(new RowCell(hc, init: imirr.getField(hc.symbol).reflectee))));
  }

  Table get table => _table;

}

class Table<E> {

  Type modelType;
  String name;
  TableHeader _theader;
  final List<Row<E>> rows;
  List<E> _es;
 
  Table(this.modelType, this._theader): rows = [], _es = [] {
    _theader.table = this;
  }
 
  factory Table.fromModelType(Type modelType) {
    TableHeader th = new TableHeader.fromType(modelType);
    Table tbl = new Table(modelType, th);
    th.table = tbl;
    return tbl;
  }
   
  TableHeader get theader => _theader;
 
  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);
  }
 
  Row<E> addRowFromEntity(E e) {
    addRow(new Row<E>.fromEntity(this, e));
  }
  // there should be more methods like these, but they should be associated with DOM operation.
  void clear() {
    _es.clear();
    rows.clear();
  }
 
  load(Iterable<E> es) {
    clear();
    _es.addAll(es);
    for (E e in es) {
      addRowFromEntity(e);
    }
  }
}

//

String getMethodName(MethodMirror md) =>MirrorSystem.getName(md.simpleName);

List<Symbol> getSymbols(Type t) => reflectClass(t).getters.values.

  fold([], (List symbs, MethodMirror mtd) =>(mtd.isGetter)?(symbs..add(mtd.simpleName)):symbs);


No comments:

Post a Comment