Saturday, March 17, 2012

Modal Dialogs with Dart

‹prev | My Chain | next›

Today, I switch back to my comic book collection app written in Dart with Hipster MVC. Currently, I have a very simple fade-in animation that displays the create comic book form:
#library('Form view to add new comic book to my sweet collection.');

#import('dart:html');
#import('https://raw.github.com/eee-c/hipster-mvc/master/HipsterView.dart');

class AddComicForm extends HipsterView {
  // ...
  render() {
    el.style.opacity = '0';
    el.innerHTML = template();

    window.setTimeout(() {
      el.style.transition = '1s';
      el.style.opacity = '1';
    }, 1);
  }
}
There is nothing fancy there—the form element is simply placed in the normal document flow. The 1 millisecond timeout seems to be necessary to give Dart a chance to render the element with the form from the template() function. Without it, the transition is ignored.

The first step for a modal dialog is the black, semi-opaque background that covers the rest of the document, ensuring that nothing else can be clicked. For that, I create a new <div> tag, position it absolutely, guess at the appropriate size, make the background color black with a slight opacity, and render it above the rest of the document. All of this can be done with the style property on Element:
  render() {
    // Black background
    Element bg = new Element.tag('div');
    document.body.nodes.add(bg);

    bg.style.position = 'absolute';
    bg.style.top = '0px';
    bg.style.left = '0px';

    bg.style.width = "1000px";
    bg.style.height = "1000px";

    bg.style.backgroundColor = 'black';
    bg.style.opacity = '0.8';
    bg.style.zIndex = '1000';

    // Display the dialog
    // ...
  }
The problem is the guess at the dimensions. Not surprisingly they only cover a portion of the document:


I could make the dimensions larger, but that will only end up adding scrollbars when they are not warranted. This means that I have to calculate dimensions. Interestingly, Dart does this inside a future (to prevent blocking during a sometimes expensive calculation). So I move the background styling into the rect property's future so that I can use the document's width and height for the modal dialog's background:
  render() {
    // Black background
    Element bg = new Element.tag('div');
    document.body.nodes.add(bg);

    window.document.rect.then((document) {
      bg.style.position = 'absolute';
      bg.style.top = '0px';
      bg.style.left = '0px';

      bg.style.width = "${document.offset.width}px";
      bg.style.height = "${document.client.height}px";

      bg.style.backgroundColor = 'black';
      bg.style.opacity = '0.8';
      bg.style.zIndex = '1000';


      // Display the dialog
      // ...

    });
  }
With that, I am ready to place my dialog:
  render() {
    // Black background
    Element bg = new Element.tag('div');
    document.body.nodes.add(bg);

    window.document.rect.then((document) {
      // Place the background above the document
      // ...

      // Place the modal dialog on top of the background
      el.style.opacity = '0';
      el.innerHTML = template();

      el.style.position = 'absolute';
      el.style.top = '0px';
      el.style.left = '0px';

      el.style.backgroundColor = 'white';
      el.style.padding = '20px';
      el.style.borderRadius = '10px';

      el.style.transition = 'opacity 1s ease-in-out';
      el.style.opacity = '1';
      el.style.zIndex = '1001';
    });
  }
That works, but the dialog is somewhat poorly placed:


To fix that, I need another rect future. This time, I grab the rect for the form dialog element so that I can use that to calculate the distance from the left side of the page:
  render() {
    // Black background
    Element bg = new Element.tag('div');
    document.body.nodes.add(bg);

    window.document.rect.then((document) {
      // Place the background above the document
      // ...

      // Place the modal dialog on top of the background
      // ...
      el.rect.then((dialog) {
        el.style.top = '10px';
        int offset_left = document.offset.width/2 - dialog.offset.width/2;

        el.style.left = "${offset_left}px";
      });
    });
  }
And that seems to do the trick:


I am undecided on how much I like the rect property. It is a little awkward, but I do appreciate the performance consideration. As for the rest, this was pretty easy to build. Best of all, I can rely on Dart to generate compatible Javascript and not have to worry (much) about IE, Mozilla, and Safari. I do think that a separate class is needed to do a modal right. Right now, I have to manually remove both the background and dialog. Something for another day.

Day #328

No comments:

Post a Comment