Skip to content
This repository was archived by the owner on Feb 22, 2018. It is now read-only.

Creating Views

Kathy Walrath edited this page Jan 17, 2014 · 2 revisions

In this chapter, we will examine how to use routing to create different views in our app. We will also clean up our app a bit by moving functionality out of our ever-growing main.dart and into more appropriate locations. Lastly, We will create a more sophisticated query service that shows you a little bit more about Futures. When you are finished, you will be able to create views within your app that are bookmarkable. You will also have a deeper understanding of how to write robust application code so that it works well in the asynchronous world of modern web apps.

###The code Here is the code for the next version of our Recipe Book app. Run the code in Dart Editor. Click on a recipe to view it. The first thing you’ll notice is some new buttons. In addition to viewing a recipe, we can now add and edit them as well. Notice how the URL changes to reflect your specific view when you navigate from recipe to recipe. Notice also that the back button now works as you would expect.

###Application structure and organization Before we dive into routing and views, lets look at the structural changes we made to the app. The first thing we’ve done is to make subdirectories for specific purposes like components, filters, services, and views. Next, we removed all of our custom components, filters, and services from the main.dart file to their own files, leaving only our main Controller, the RecipeBookController inside main.dart.

###Encapsulating view logic into components We also created two more components: SearchRecipeComponent and ViewRecipeComponent. These are examples of how components can be used to encapsulate view specific functionality. The RatingComponent is a little bit different in that it is truly generic and can be used anywhere. For this reason, we put it in a separate directory (lib/rating).

Our two new components aren’t generic. They contain logic that is specific to the view they control and so they live in the view directory.

Lets look at SearchRecipeComponent and the HTML that goes with it. The component takes two attributes that should look familiar: nameFilterString and categoryFilterMap. The component contains just enough logic to be able to control the portion of the view responsible for searching and filtering. Note that both attributes are declared as bidirectional. This is because the SearchRecipeComponent clears the search filters by resetting these two attributes.

Now our app’s main index.html file is a lot simpler. Instead of containing all the markup to set up the search and filter views, it now just contains the call to the component. The details of the view elements are hidden away in the RecipeSearchComponent’s HTML template.

<search-recipe 
    name-filter-string="ctrl.nameFilter"
    category-filter-map="ctrl.categoryFilterMap">
</search-recipe>

###Using routing to create views In earlier versions of our app, we had only one view. No matter where you were in the app, the URL never changed to reflect what you were doing. This is generally undesirable and user unfriendly. If one of our users found a recipe they liked, they had no way of bookmarking it for later. Using routing to create views lets us address this problem. Routing makes it easier to build large single-page applications. It lets you map the browser address bar to the semantic structure of your application, and keeps them in sync.

####Configuring the router Let’s look at recipe_book_router.dart. There we define all of our app’s routes and map them to views. To take advantage of routing, just create a class that implements the RouteInitializer interface. Angular will look for an instance of RouteInitializer when it instantiates the router and will use it to configure the routes. RouteInitializer has a single method: init. It takes two parameters, a Router, and a ViewFactory, both of which are created by the dependency injector for you.

class RecipeBookRouteInitializer implements RouteInitializer {
  init(Router router, ViewFactory view) {

All you need to do to set up routing is to implement the init method, and then make the RouteInitializer available in the your app’s module configuration:

type(RouteInitializer, implementedBy: RecipeBookRouteInitializer);

Here is a simple route configuration. Let’s examine the call to addRoute, and all of its parameters in detail:

router.root
    ..addRoute(
      name: 'add',
      path: '/add',
      enter: view('view/addRecipe.html'))

#####name Is the name you give to the route. You will use this name in the template to tell Angular which route to use for the view.

#####path Is the URL pattern that maps to this route. More than one route can match a given pattern, so patterns should go from more specific to more general. (Hint: if your routes are misbehaving, this is probably the place to start looking for misconfiguration). If you find you need to debug your routing configuration, import Dart’s logging library and add this magic to your main():

Logger.root.level = Level.FINEST;
Logger.root.onRecord.listen((LogRecord r) { print(r.message); });

#####enter A RouteEventHandler that tells the router what to do when the route is entered. Our example code above asks the ViewFactory to create a RouteEventHandler that points our route to a specific html file. You can also write your own custom RouteEventHandlers.

#####leave A RouteEventHandler that tells the router what to do when the route is left. You can use this for a number of purposes. For example, you can determine if a view is leavable, and what to do if it shouldn’t be left (for example, if there are unsaved changes on a view, you might want to prevent the route from leaving, or warn the user that their changes will be lost).

#####defaultRoute Specifies whether this route is the default route.

####Routes can do more than just load a view The configuration below is an example of a more complex enter handler. It uses the defaultRoute and a special RouteEventHandler to redirect users to something sane in the event that they entered an invalid URL. In our case, it routes them back to the “view recipe” view. The replace: true causes the invalid route to be removed from the history, so going back doesn’t cause your users to go through an invalid route.

..addRoute(
    name: 'view_default',
    defaultRoute: true,
    enter: (_) =>
        router.go('view', {'recipeId': ':recipeId'},
            startingFrom: route, replace: true))

####Routes are hierarchical The mount parameter allows you to define nested routes. The URL for each nested route is created by appending it’s path to its parents’ paths. Here is an example of nested paths in our app:

..addRoute(
    name: 'recipe',
    path: '/recipe/:recipeId',
    mount: (Route route) => route
        ..addRoute(
            name: 'view',
            path: '/view',
            enter: view('recipeView.html'))

Our app’s nested paths are a little more complicated, because they also add a parameter to the path. In the example above, the all the routes underneath the recipe subtree require a recipeId parameter.

Here is how our routing configuration will build the paths we’ve defined:

...#
...#/add
...#/recipe/6/view
...#/recipe/6/edit

####Connecting a route to a view Now lets look at the how the routes and views are connected in our app. There are two pieces involved: the router configuration, and the ng-view Directive. Open up the index.html file and look at the details section. It contains only an ng-view Directive.

<section id="details">
    <ng-view></ng-view>
</section>

The ng-view Directive causes the rendered template of the current route to be included into the layout where the ng-view appears. Whenever the current route changes, the ng-view changes the view.

Next we’ll look at how to make the route parameters available inside our ViewRecipeComponent.

We’ve added a parameter for the RouteProvider to its constructor, which we use to extract the parameters for the current route. In our case, we extract the recipeId to look up the recipe being viewed:

ViewRecipeComponent(RouteProvider routeProvider) {
  _recipeId = routeProvider.parameters['recipeId'];
}

###More on Futures In this version of our app, we also separated the query layer out from our main app by creating a QueryService. The QueryService does basically the same thing that our old _loadData method did, but with a couple of enhancements.

First, it provides getters for common queries. Notice that these getters don’t return concrete objects. They return Futures. Our application code still has to plan for the possibility that the service will be slow, so our app’s _loadData method isn’t too different from the last version. It still determines if the data is loaded and sets appropriate view messages. It just delegates to the QueryService to do the actual query and JSON mapping.

Second, we’ve built some caching into the QueryService. This (overly simplistic) cache determines if the data has already been loaded, and if so, serves it from the cache. The service guarantees that data is loaded by using Future chaining. Let’s look at the QueryService constructor:

  QueryService(Http this._http) {
    _loaded = Future.wait([_loadRecipes(), _loadCategories()]);
  }

It wraps the calls to _loadRecipes and _loadCategories in a call to Future.wait. Future.wait also returns a Future which completes only when all the Futures in the list complete.

The getters in the service (getRecipeById, getAllRecipes, getAllCategories) first check to see if the cache has finished being loaded. If not, it returns a Future that will wait until _loaded is complete. If the cache has been populated, it still returns a Future - a new Future with the value of the cached data.

Here’s an example:

Future<Recipe> getRecipeById(String id) {
  if (_recipesCache == null) {
    return _loaded.then((_) {
      return _recipesCache[id];
    });
  }
  return new Future.value(_recipesCache[id]);
}

###Now its your turn We have set up a stub for adding and editing recipes. Try building the view to edit an existing recipe and write it back out to the mock data store (the JSON file). First you’ll need to create a view component for editing a recipe. Next, you will need to create a method on the QueryService to update the recipe data store, and then reload the recipe cache from the data store.

####Extra credit Try to use the router leave() callback to prevent a user from leaving a page with unsaved edits.

Home | Prev

Clone this wiki locally