Thursday, February 18, 2016

Delegated Events Chain of Responsibility


I came up with a simple DOM-based example of the chain of responsibility pattern last night. That might serve as an simple introduction to the pattern on the modern web, but I would prefer something with a little more heft to support subsequent discussions. So tonight, I attempt to apply the pattern to cell formatting object.

Given a bunch of cells in a table, I would like a series of objects to format input values correctly. Numbers should have 2 decimal places and be right-aligned. Dates should be iso8601 formatted and left aligned. Everything else should be treated as plain text and left aligned:



The handler in this chain of responsibility implementation is going to be a listener for change events on the container:
  container.onChange.listen(handler.processRequest);
I do not want a separate listener for each cell—that could wind up being a very large number of cells and listeners. Instead, I use a delegated events approach, listening on the container and filtering events based on the target.

The handler in this example will be the CellFormatter. As with any chain of responsibility implementation, the first thing that it needs is a way to specify the next link in the chain. In this case, the next handler:
abstract class CellFormatter {
  CellFormatter nextHandler;
  // ...
}
Next, I need to process events:
abstract class CellFormatter {
  // ...
  void processRequest(Event e) {
    if (!isCell(e)) return;
    if (handleRequest(e)) return;
    if (nextHandler == null) return;

    nextHandler.processRequest(e);
  }

  bool handleRequest(Event e) => false;
  // ...
}
First, I filter on cells (text input fields). Then I try to handle the current request. The default implementation here is for handleRequest to not handle the request, which it signals by returning false. It will be the job of the concrete subclasses to properly handle requests. If the request is not handled, then the request sent to the next object in the chain, if defined.

With that, I am ready to process different formatting, like NumberFormatter:
class NumberFormatter extends CellFormatter {
  bool handleRequest(Event e) {
    var input = e.target;
    RegExp exp = new RegExp(r"^\s*[\d\.]+\s*$");
    if (!exp.hasMatch(input.value)) return false;

    var val = double.parse(input.value);
    input.value = val.toStringAsFixed(2);
    input.style.textAlign = 'right';
    return true;
  }
}
If the current change event targets an input that does not match the numeric regular expression, handleRequest() returns false and the event is sent along to the next formatter object in the chain. If it does point to a numeric input element, the value is parsed and formatted with two digits after the decimal point. The text alignment of the input element is set appropriately for a numeric field. And that's it.

Other formatters follow similar approaches until the last item in the chain, the default text formatter. This formatter applies no formatting to the value, but ensures the left alignment of the field:
class TextFormatter extends CellFormatter {
  bool handleRequest(Event e) {
    var input = e.target;
    input.style.textAlign = 'left';
    return true;
  }
}
Armed with my formatters, I can create them in client code:
  var number = new NumberFormatter();
  var date = new DateFormatter();
  var text = new TextFormatter();
Then, I establish the successor chain:
  number.nextHandler = date;
  date.nextHandler = text;
That is all there is to it. Now when the event listener sends a request to the first item in the chain:
  container.onChange.listen(number.processRequest);
I know that one of the handlers will receive and process the event. I think that will be a reasonably accessible example for most readers. I may fiddle a bit more with the example tomorrow to be sure.

Play with the code on DartPad: https://dartpad.dartlang.org/21f67a92a0269a2aed6b.


Day #99

No comments:

Post a Comment