Pages

Exploring Latest PolymerDart SPA using Custom Elements

Thursday, April 2, 2015

Recently, PolymerDart team has released its version 0.1.6, and they have published another sample: the SPA example. It is based on PolymerJS SPA Example, and it is pretty nice starting point to build practical SPA in Dart. So I have been exploring the sample to understand the architecture and found some parts of what I wanted to add and modify to make it a bit more practical for me. I am not sure it is the right way, but try to share what I have done here.

Add custom page element for every page.

The original sample only shows content of every page name by <div>{{page.name}}</div> in the main pane. For real app I want to use custom element to handle the entire page content.

There are five pages in the sample(#one, #two, #three, #four, #five), so I also create five corresponding custom elements(one-page, two-page, three-page, four-page, five-page).

lib/src/elements.dart

// Page elements
export 'package:polymer_spa_example/one_page.dart';
export 'package:polymer_spa_example/two_page.dart';
export 'package:polymer_spa_example/three_page.dart';
export 'package:polymer_spa_example/four_page.dart';
export 'package:polymer_spa_example/five_page.dart';

lib/one_page.dart

@HtmlImport('src/one_page.html')
library one_page;

import 'package:polymer/polymer.dart';

@CustomTag('one-page')
class OnePage extends PolymerElement {

  OnePage.created() : super.created();

}

lib/src/one_page.html

<polymer-element name="one-page" layout vertical center-center fit>
  <template>
    <link rel="stylesheet" href="one_page.css">
    <div>Single</div>
  </template>
</polymer-element>

Of course we will need to consider directory structure for custom elements when a project code grows.

Insert custom element at route change.

Then I try to insert a page element whenever user enters a new route. And make sure contents are cleared before the insertion.

lib/example_app.dart

/// Updates [route] whenever we enter a new route.
void enterRoute(RouteEvent e) {
  route = e.path;
  /// Ensure to clear page element, and add the page element corresponding to route.
  if (route != null && route != "") {
      corePages.querySelector('section[hash="$route"]').children
          ..clear()
          ..add(new Element.tag("${route}-page"));
  }
}

I create a convention that the custom element name must be ${route}-page (new Element.tag("${route}-page")). This is a quick hack and looks a little dirty. I modify this part in the later section.

Handling custom page elements between page transition animation.

I think there might be some edge cases, but usually in SPA, on moving to new page, the previous page content is expected to be deleted from DOM tree to lighten its load.

But simply deleting page content immediately after the page transition start works but disrupts the smooth transition animation between pages. This is bad for UX. To fix it, it needs to keep leaving page's content until the page transition animation finishes showing a new page. So I add var _previousRoute; to keep leaving page's route, and also add the code to handle corePage's onTransitionEnd event for the timing to delete. (Borrowing the idea from erikringsmuth/app-router)

void handlePageElementsOnRouteTransition() {
  // Clear previous route's content on the transition end.
  // Following app-router's idea.
  // https://github.com/erikringsmuth/app-router/blob/master/src/app-router.js
  // TODO: This doesn't work well when another transition starts before a transition ends. Needs another tweak.
  corePages.onTransitionEnd.listen((TransitionEvent e) {
    if (_previousRoute != null && _previousRoute != route) {
      corePages.querySelector('section[hash="$_previousRoute"]').children.clear();
    }
  });
}

It works.

demo

Make it more explicit.

As I said in previous section, "${route}-page" looks a little hacky. I prefer more explicit way so I add customTag attribute and Element create() => new Element.tag("$customTag"); in Page class to handle corresponding custom page element with page instance itself.

example_app.dart

/// Simple class which maps page names and custom tags to paths.
class Page {
  final String name;
  final String path;
  final String customTag;
  final bool isDefault;

  const Page(this.name, this.path, this.customTag, {this.isDefault: false});

  // Consider some conventions. For example, custom tag name is expected to be same as the name...
  Element create() => new Element.tag("$customTag");

  String toString() => '$name';
}

/// The list of pages in our app.
final List<Page> pages = const [
  const Page('Single', 'one-page', 'one-page', isDefault: true),
  const Page('page', 'two-page', 'two-page'),
  const Page('app', 'three-page', 'three-page'),
  const Page('using', 'four-page', 'four-page'),
  const Page('Polymer', 'five-page', 'five-page'),
];

/// Updates [route] whenever we enter a new route.
void enterRoute(RouteEvent e) {
  route = e.path;
  if (selectedPage == null) selectedPage = pages.firstWhere((page) => page.path == route);
  // Ensure to clear page element, and add the page element corresponding to route.
  if (route != null && route != "") {
    corePages.querySelector('section[hash="$route"]').children
      ..clear()
      ..add(selectedPage.create());
  }
}

Non hash fragment mode (HTML5-mode by Angular)

If you prefer to delete # in the url (means to change to non hash fragment mode, or 'HTML5-mode' by Angular), you can simply change the Router's useFragment option to false (new Router(useFragment: false);). Note, it needs some reverse proxy server (like Nginx) settings to handle routes.

References

You can see the whole source code on Github

No comments:

Post a Comment