Showing posts with label agularjs. Show all posts
Showing posts with label agularjs. Show all posts

Nested Views, Routing, And Deep Linking With AngularJS

Out of the box, AngularJS gives you routing and the ngView directive. With these two features, you have the ability to map routes onto templates that get rendered inside of the ngView container. This works for simple, one-dimensional web sites; but, unfortunately, if you have a site that requires deep routing, AngularJS leaves you up to your own devices. In order to achieve deep routing, I've found it more useful to map routes onto action variables rather than templates; this gives you a great degree of flexibility and makes creating nested, independent views much easier.


When I think about rendering a page in an AngularJS application, I think in terms of two parallel concepts:
  • Request Context
  • Render Context
The Request Context is the result of route mapping. The request context contains the route parameters and the "action" onto which the route was mapped. The action variable is a dot-delimited list of values that tells the rendering engine which templates to render. In the "Adopt-a-Pet" demo, for example, the action value for a pet's medical history is this:
standard.pets.detail.medicalHistory
Each item in the action list represents a view within the page that [generally speaking] has a corresponding controller instance.
Graphically, you can think about the relationship between the Request Context and the Render Context as such:


 AngularJS routing using Request Context and Render Context.
The Render Context is the portion of the Request Context that pertains to a given, nested view. Because the Render Context is a subset of the Request Context, the render context doesn't necessarily need to react to all changes in the Request Context. In fact, the Render Context only needs to react when relevant portions of the Request Context change.
A stripped-down Controller instance would look like this:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
(function( ng, app ){
"use strict";
app.controller(
"pets.detail.DetailController",
function( $scope, requestContext, _ ) {
// ...
// Other methods defined here.
// ...
// Get the render context local to this controller (and relevant params).
var renderContext = requestContext.getRenderContext( "standard.pets.detail", "petID" );
// --- Define Scope Variables. ---------------------- //
// Get the relevant route IDs.
$scope.petID = requestContext.getParamAsInt( "petID" );
// The subview indicates which view is going to be rendered on the page.
$scope.subview = renderContext.getNextSection();
// --- Bind To Scope Events. ------------------------ //
// I handle changes to the request context.
$scope.$on(
"requestContextChanged",
function() {
// Make sure this change is relevant to this controller.
if ( ! renderContext.isChangeRelevant() ) {
return;
}
// Get the relevant route IDs.
$scope.petID = requestContext.getParamAsInt( "petID" );
// Update the view that is being rendered.
$scope.subview = renderContext.getNextSection();
// If the relevant ID has changed, refresh the view.
if ( requestContext.hasParamChanged( "petID" ) ) {
loadRemoteData();
}
}
);
// --- Initialize. ---------------------------------- //
// Load the "remote" data.
loadRemoteData();
}
);
})( angular, Demo );
view rawcontroller.js hosted with ❤ by GitHub
As you can see, the "render context" for the given controller involves the action path, "standard.pets.detail" and the route param, "petID". A render context can listen to any portion of the action as well as any subset of route parameters. When the request context changes, the application controller broadcasts the "requestContextChanged" event. The controller listens for this event and checks to see if the change is relevant to the current "render context":
123456
// Make sure this change is relevant to this controller.
if ( ! renderContext.isChangeRelevant() ) {
return;
}
view rawcontroller-misc.js hosted with ❤ by GitHub
If the request context change is relevant to the given render context, the Controller may choose to take action (such as reloading any remote data relevant to the rendered view).
If you look at the code, you can see that the renderContext instance exposes the method, getNextSection(). This method returns the next value in the action chain, relative to the render context. So, for example, if the route action was:
standard.pets.detail.medicalHistory
... and the render context location was:
standard.pets.detail
... a call to getNextSection() would return, "medicalHistory." This ability to see the next action in the action path allows the relevant view to figure out which subview to render (if there are any).
A stripped-down View would then look like this:
123456789101112131415161718
<div ng-controller="pets.detail.DetailController">
<!-- More code here. -->
<!-- Include SubView Content. -->
<div ng-switch="subview">
<div ng-switch-when="background" ng-include=" ... "></div>
<div ng-switch-when="diet" ng-include=" ... "></div>
<div ng-switch-when="medicalHistory" ng-include=" ... "></div>
</div>
<!-- More code here. -->
</div>
view rawview.htm hosted with ❤ by GitHub
As you can see, we use the ngSwitch and ngSwitchWhen directives to render the appropriate subview based on the render context.
I've only been using this approach for a few months; but so far, I've been extremely pleased with the results. This Request Context / Render Context construct allows Controllers to change only when they need to, and no more. Furthermore, I believe it helps you think about Controllers as decoupled instances that rely more on the route data and less so on the inherited data. While this can call for some redundant data collection, it greatly simplifies your rendering logic.