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

3

23

I am going to build complex application having many different views. Imagine for example eshop solution. There can be a lot of different views:

  • Contact page with some static information
  • Registration form for new customers
  • View of your order
  • List of the products
  • Details about a product
  • ...

Now I am little bit confused, how to build such a complex application using Web UI. I would like to have the HTML templates for the views separated in multiple files and having some logic to determine, which one should be rendered.

Assume I want to have a single master template containing basic things like header and footer, then I have a lot of content templates, these should get injected into the right place inside the master template.

Until now, I have always seen only small single-template examples of using Dart Web UI, so I have no idea, how to achieve this.

Malefic answered 24/4, 2013 at 9:29 Comment(0)
A
29

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 view
ComponentItem lifecycleCaller;

/// Switches to contacts view
void 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 view
void 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.

Argosy answered 24/4, 2013 at 12:23 Comment(5)
Personally I don't instantiate completely with web_ui, using templates with the if attribute connected to a top level function: <template if="display('products')><x-products></x-products></template> to determine what areas should be displayed. In addition for this use case, I would also have 'sub views' so to speak. For instance my products.html may import products_details.html and products_list.html.. depending on complexity required.Jahnke
and how do you pass data from one view (products.dart) to another (contacts.dart) ? As I understand user should be able to choose something in 'products.dart' then he/she click 'contacts' and see the list of chosen products. How to pass List<Product> to another view ?Fimbriate
@Jasper To pass data to the products view you could create a constructor ProductsView(List<Product> products) inside products.dart and instantiate the ProductsView with this constructor.Argosy
Or if you are using the template solution I mentioned, you can add an optional attribute to your products: <x-products prodList="{{selectedProds}}"></x-products>Jahnke
Got the source, added the yaml and ran it: when clicking on "show contact view" I get an error. any idea why: "Breaking on exception: Bad state: No elements" in iterable.dart in source: E get single { Iterator it = iterator; if (! it.moveNext()) throw new StateError("No elements"); E result = it.current; if (it.moveNext()) throw new StateError("More than one element"); return result; }Sped
B
16

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 on
void 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.

Benefaction answered 25/4, 2013 at 4:12 Comment(1)
It looks like your solution is no longer valid. The web_ui packages are now called polymer and the watcher stuff seems to exist no longerCathern
H
2

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'];
  }
}
Haemoglobin answered 10/8, 2014 at 11:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.