How to build a complex Web UI application with multiple views? How to build a complex Web UI application with multiple views? dart dart

How to build a complex Web UI application with multiple views?


I've put together a little example of how I currently do it (hope we will soon see a larger best practice example application for this):

For the complete source code of this example see gist: How to build a Web UI application with multiple views in Dart

Main Application

  • app.html - Contains the main application layout, instantiates the header and footer component and creates a container for the views.
  • app.dart - Handles navigation events and replaces the view inside the view container (see below)
  • app.css

Web Components

Header and Footer

  • header.html - Web Component for header
  • footer.html - Web Component for footer

Views

  • contact.html - Web Component for the Contacts View
  • contact.dart - Dart file containing ContactsView class
  • products.html - Web Component for the Products View
  • products.dart - Dart file containing ProductsView class

Switching Between Views

The standard way to instantiate Web Components is by using <x-foo></x-foo> in HTML.As we have different views, we will have to instantiate the Web Components inside our Dart code. Doing this we have to manually call the Web Components lifecycle methods. This is not straight forward and might be improved in the future (see Issue 93 which also contains some exmples).

Here is how you can switch views (source of app.dart):

import 'dart:html';import 'package:web_ui/web_ui.dart';import 'contact.dart';import 'products.dart';void main() {  // Add view navigation event handlers  query('#show-contact-button').onClick.listen(showContactView);  query('#show-products-button').onClick.listen(showProductView);}// Used to call lifecycle methods on the current viewComponentItem lifecycleCaller;/// Switches to contacts viewvoid showContactView(Event e) {  removeCurrentView();  ContactView contactView = new ContactView()      ..host = new Element.html('<contact-view></contact-view>');  lifecycleCaller = new ComponentItem(contactView)..create();  query('#view-container').children.add(contactView.host);  lifecycleCaller.insert();}/// Switches to products viewvoid showProductView(Event e) {  removeCurrentView();  ProductsView productsView = new ProductsView()      ..host = new Element.html('<products-view></products-view>');  lifecycleCaller = new ComponentItem(productsView);  lifecycleCaller.create();  query('#view-container').children.add(productsView.host);  lifecycleCaller.insert();}void removeCurrentView() {  query('#view-container').children.clear();  if (lifecycleCaller != null) {    // Call the lifecycle method in case the component needs to do some clean up    lifecycleCaller.remove();  }}

And here is the source for app.html:

<!DOCTYPE html><html>  <head>    <meta charset="utf-8">    <title>A Complex Web UI Application</title>    <link rel="stylesheet" href="app.css">    <!-- import the header and footer components -->    <link rel="components" href="header.html">    <link rel="components" href="footer.html">    <!-- import the view components -->    <link rel="components" href="contact.html">    <link rel="components" href="products.html">  </head>  <body>    <header-component></header-component>    <div id="view-container"></div>    <button id="show-contact-button">show contact view</button>    <button id="show-products-button">show products view</button>    <footer-component></footer-component>    <script type="application/dart" src="app.dart"></script>    <script src="packages/browser/dart.js"></script>  </body></html>

Note: I had to import the view components via <link rel="components" href="contact.html"> even though I do not directly reference it in the HTML file.


You can use the route library combined with templates to greatly automate the process.

In urls.dart you will define the routes that the app will handle. app.dart will setup the route listener. Lastly, app.html will hold a page container that will automatically switch the page component (through the use of template instantiation).

With this structure set up, page navigation can be handled through regular anchor tags instead of calling custom functions to change the page.

In order to add a new page you will have to do the following:

  1. Add a new route in urls.dart
  2. Create a new WebComponent in the pages/ folder
  3. Add a new conditional template for the page in app.html

Below you can see an example of an app that handles a home page and a contact page:

urls.dart:

library urls;import 'package:route/url_pattern.dart';final homeUrl = new UrlPattern(r'/');final contactUrl = new UrlPattern(r'/contact');

app.dart:

import 'dart:html';import 'package:web_ui/web_ui.dart';import 'package:route/client.dart';import 'urls.dart' as urls;import 'package:web_ui/watcher.dart' as watchers;  // Setup the routes to listen to    void main() {  var router = new Router()  ..addHandler(urls.homeUrl, showPage)  ..addHandler(urls.contactUrl, showPage)    ..listen();}// Change the page that we are onvoid showPage(String path) {  watchers.dispatch();}

app.html

<!DOCTYPE html><html>  <head>    <meta charset="utf-8">    <title>Sample app</title>    <link rel="stylesheet" href="app.css">    <!-- import the pages -->    <link rel="components" href="pages/xhomepage.html">    <link rel="components" href="pages/xcontactpage.html">  </head>  <body>    <!-- You could put a header here if you want !-->    <!-- Templates take care of automatically switching the page !-->    <div class="pages">          <template instantiate="if urls.homeUrl.matches(window.location.pathname)">        <x-home-page></x-home-page>      </template>      <template instantiate="if urls.contactUrl.matches(window.location.pathname)">        <x-contact-page></x-contact-page>      </template>    </div>    <!-- You could put a footer here if you want !-->    <script type="application/dart" src="app.dart"></script>    <script src="packages/browser/dart.js"></script>  </body></html>

Edit: I've removed the step where app.dart has to define its own pages. Instead, templates check to see if the URL path matches the UrlPattern defined in urls.dart. This should simplify things a bit more.


I created a Polymer element <bind-view> that creates and adds a view element depending on the current route. The element works with the route_hierarchical package.
See BWU Polymer Routing on GitHub for more details.

A route configuration looks like

library bwu_polymer_routing_example.route_initializer;import 'package:route_hierarchical/client.dart' as rt;import 'package:bwu_polymer_routing/module.dart';class RouteInitializer implements Function {  void call(rt.Router router, RouteViewFactory views) {    views.configure({      'usersList': routeCfg(          path: '/users',          view: 'user-list',          defaultRoute: true,          dontLeaveOnParamChanges: true,          enter: (route) => router.go('usersList', {})),      'user': routeCfg(          path: '/user/:userId',          view: 'user-element',          dontLeaveOnParamChanges: true,          mount: {        'articleList': routeCfg(            path: '/articles',            view: 'article-list',            defaultRoute: true,            dontLeaveOnParamChanges: true,            mount: {          'article': routeCfg(              path: '/article/:articleId',              view: 'article-element',              bindParameters: ['articleId', 'userId'],              dontLeaveOnParamChanges: true,              mount: {            'view': routeCfg(                path: '/view',                defaultRoute: true,                dontLeaveOnParamChanges: true),            'edit': routeCfg(                path: '/edit',                dontLeaveOnParamChanges: true)          })        })      })    });  }}

the <app-element> contains the <bind-view> element, a placeholder where the view configured for the current route gets added.Views can be nested. Any view can itself contain a <bind-view> element. This allows to create hierarchical view composition without much boilerplate.

<!DOCTYPE html><link rel='import' href='../../../../packages/polymer/polymer.html'><link rel='import' href='../../../../packages/bwu_polymer_routing/bind_view.html'><link rel='import' href='user_list.html'><link rel='import' href='user_element.html'><link rel='import' href='article_list.html'><link rel='import' href='article_element.html'><polymer-element name='app-element'>  <template>    <bind-view id='app-element'></bind-view>  </template>  <script type='application/dart' src='app_element.dart'></script></polymer-element>

The app_element.dart file contains the router initialization code

class AppModule extends Module {  AppModule() : super() {    install(new RoutingModule(usePushState: true));    bindByKey(ROUTE_INITIALIZER_FN_KEY, toValue: new RouteInitializer());  }}@CustomTag('app-element')class AppElement extends PolymerElement with DiContext {  AppElement.created() : super.created();  @override  void attached() {    super.attached();    initDiContext(this, new ModuleInjector([new AppModule()]));  }}

The package also contains some helper mixins to add dependency injection (DI) functionality to Polymer elements like the DiContext mixin used here.Constructor injection can't be used with Polymer but events are a good substitute.

The DiConsumer mixin allows to request an instance from DI with this simple code

@CustomTag('article-list')class ArticleList extends PolymerElement with DiConsumer {  @observable String userId;  @override  void attached() {    super.attached();    // The two lines below show how to request instances from DI    // but they are redundant here because     // route parameters are assigned to attributes of the view automatically    // when the view is created or when the values change    var di = inject(this, [RouteProvider /* add more types here as needed */]);    userId = (di[RouteProvider] as RouteProvider).parameters['userId'];  }}