-
Notifications
You must be signed in to change notification settings - Fork 83
Creating Views
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.