Routing ####### The *routes* ************ Inside *Modules* and *Components* there is access to a pre-defined attribute:: self.router This gives access to the routing engine, which is controlled by *routes* definitions given in the topmost module and *sub-routes* in sub-modules thereof. Notice that unlike *bindings* and *services* which can be defined in both *Modules* and *Components*: **routes cannot be defined in components**. Let's see a real definition from the *Tour of Pyroes* :doc:`/tutorial/top5/index` sample. .. code-block:: python routes = [ { 'path': '', 'redirect_to': '/dashboard', 'path_match': 'full' }, { 'path': 'dashboard', 'component': DashboardComponent }, { 'path': 'pyroes', 'component': PyroesComponent }, { 'path': 'detail', 'component': PyroDetailComponent, 'params': {'pyd': int}, # param transformation function }, ] As you may see above, the routes is a *list* (or iterable) of *dict* entries. Let's try to bring light to the definitions. The most usual aspect for an entry will probably be this. .. code-block:: python { 'path': 'dashboard', 'component': DashboardComponent }, The route defines a: - ``path``, i.e.: the matching pattern for the route. In this case we have ``dashboard``, so that this will be matched:: baseurl/dashboard Obviously, we don't have to care about ``baseurl`` in the definition of the route, because our app can be located and relocated to different servers, paths. - ``component``, i.e.: which component will be instantiated when the route is a match. Actually, a component is **not** always instantiated. Ideally this will only happen once. All other times when navigating to/from the route, the component (and the associated html) will be *loaded* and *unloaded*. See :doc:`/architecture/component` for details about ``loading`` and ``unloading``. .. note:: Although not shown here, the code in :doc:`/tutorial/top5/index` contains an import statement for ``Dashboardcomponent`` to make it visible inside the module and avoid an error. Route Rendering =============== When navigating to the route and the route is a match and a component has to be rendered on-screen, the following (basically) happens - Within the hierarchy of the current component (if any) a ```` tag is sought. - If none is found, the default behavior is to generate one in-place. - Be it the found one or the generated one, the ```` tag becomes the parent node for the rendering of the component. - The component is then given the chance to render itself underneath ````. - The component is told it is ``loading`` into the DOM by calling the method with the same name. When navigating away from the route the following (basically) happens - The component is told it is ``unloading`` from the DOM by calling the method with the same name. - The ```` tag is emptied (the component's html is removed from the DOM). Additionally: - Navigating to and away from routes can be controlled with authentication guards/deactivation controls. More on that later. Defining parameters for routes ============================== .. code-block:: python { 'path': 'detail', 'component': PyroDetailComponent, 'params': {'pyd': int}, # param transformation function }, This component adds a parameter entry:: 'params': {'pyd': int}, # param transformation function Which includes a remark that ``{'pyd': int}`` is a transformation function. A URL is a text string and parameters are therefore when parsed also *text*. The possibility to specify a transformation function means that components don't have to care about the details. The details are being taken care of for them. A sample URL with parameters (also from the tutorial sample reference above):: http://127.0.0.1:2222/detail;pyd=13 Before our ``PyroDetailComponent`` is given a chance to act, the *router* knows it has to parse the parameter and transform that to an ``int``. The parameter is later available for the component under ``self.params``. Actual code from the sample. .. code-block:: python def loading(self): self.pyro_service \ .get_pyro(self.params.get('pyd', 0)) \ .subscribe(self.pyro_) # fetch async and fire self.pyro_ when done As anyone can guess: ``self.params`` is a *dict*. Hence possibility to do:: self.params.get('pyd', 0) The reader may have been expecting this though:: self.params['pyd'] But this wouldn't work if someone had wrongly pasted this URL in the browser:: http://127.0.0.1:2222/detail Because there would be no ``pyd`` parameter. And that's why the code tries to play nice and issue a ``get('pyd', 0)`` **Rest Matching and Redirection** There is another entry which looks different in the above *routes* definition .. code-block:: python { 'path': '', 'redirect_to': '/dashboard', 'path_match': 'full' }, The ``path`` definition is empty and a ``'path_match': 'full'`` is given which translates to - If nothing else remains to be matched in the route, then do the defined action In this case it does not load a component but specifies a:: 'redirect_to': '/dashboard', to redirect to a different path. Notice that - The route definitions have no leading ``/`` because they are transformed internally to be ``baseurl/path`` (if a leading ``/`` is specified, it will be stripped internally to ensure proper route construction) - But when redirecting, we could be issuing a *relative* redirect (relative to the current path) or an absolute. In this case the redirect is absolute Child Routes ============ Via the *routes* definition --------------------------- Yes, they are also possible. Let's see the routes from the :doc:`/tutorial/tourer/index` sample. .. code-block:: python routes = [ { 'path': 'compose', 'component': ComposeMessageComponent, 'outlet': 'popup' }, { 'path': 'disaster-center', 'load_children': [DisasterCenterModule] }, { 'path': 'admin', 'load_children': [AdminModule] 'can_activate', [AuthGuard], }, {'path': '', 'redirect_to': '/superpyroes', 'path_match': 'full'}, {'path': '*', 'component': PageNotFoundComponent}, ] The key here is the ``load_children``:: { 'path': 'disaster-center', 'load_children': [DisasterCenterModule] }, In this case there is only 1 sub-module: ``DisasterCenterModule``. Any routes defined in this *module* will be loaded and treated as child routes of our main module. And - They will be available under the defined ``path``:: 'path': 'disaster-center', Benefits and goals of this: - Breaking the functionality into different, separated and isolated pieces to simplify and facilitate development - Having full modules which can act as a main module or as a sub-module. The ``DisasterCenterModule`` could be conceived as a complete application, which in this case is being created under the main ``AppModule``. - Being able to replace the ``DisasterCenterModule`` with something else very easily thanks to separation and isolation Cascading Child Routes +++++++++++++++++++++++++ The reasoning about including a fully-fledged module (``DisasterCenterModule``) as a sub-module of another module, opens up the possibility that the sub-module could have its own child routes defined. And yes, it is. From that sample, the set of routes defined by ``DisasterCenterModule`` .. code-block:: python class DisasterCenterModule(Module): services = { 'disaster_service': DisasterService, } routes = [{ 'path': '', 'component': DisasterCenterComponent, 'children': [ { 'path': '', 'component': DisasterListComponent, 'children': [ { 'path': '', 'component': DisasterDetailComponent, 'params': {'did': int}, # transformation function }, { 'path': '', 'component': DisasterCenterHomeComponent, } ] } ] }] It's not only that it is defining *children*, it is already nesting them. In this case not *loading* them from any other module but simply defining them as ``children``. In this manner, one can define a hierarchy of components. See:: 'path': '', 'component': DisasterCenterComponent, 'children': [ Which translates to: - As a route I am adding nothing to the ``path``, so the current match is valid. - As a route I am simply saying that a component named ``DisasterCenterComponent`` has to be loaded - And please be aware that I have ``children`` If we carry on:: 'path': '', 'component': DisasterListComponent, 'children': [ There is a second iteration which is exactly like the previous. The most important part in both definitions is, possibly, that they are **adding nothing to the path** and therefore have no influence for the matching process. Further descending, the final children:: { 'path': '', 'component': DisasterDetailComponent, 'params': {'did': int}, # transformation function }, { 'path': '', 'component': DisasterCenterHomeComponent, } Both again ... have no content for ``path``. This at the end of the day means that they add nothing for the matching when it comes down to the ``path``. The main path will be valid for both. But notice that:: 'params': {'did': int}, # transformation function There is a ``params`` definition for the first of the two chilren. This is the tie breaker. - If there is param and matches ``did`` our first child and the component ``DiasterDetailComponent`` will be a winner - If there is no param or match, our 2nd child wins and the ``DisasterCenterHomeComponent`` will be loaded Via the *module* directive -------------------------- In that same sample, the following is made to load child routes .. code-block:: python class AppModule(Module): components = AppComponent modules = PyroesModule, LoginModule ... In this case two sub-modules are being added to the hierarchy of ``AppModule``. And every route defined in those components will be made a child route of the main routes definition. Of course, and because no ``path`` definition is possible: - The child routes from ``PyroesModule`` and ``LoginModule`` will be made available under the root path: ``/``. Non matching routes =================== From the :doc:`/tutorial/tourer/index` sample:: {'path': '*', 'component': PageNotFoundComponent}, Use ``'*'`` or ``'**'`` to mark a route which will take over if all other routes were not a match for the current path. In this case the action is: - Load the component ``PageNotFoundComponent`` It could also have been a redirect to the root path as in:: {'path': '*', 'redirect_to': '/'}, Guards - Route Activation ************************* A route can be **guarded** from entering/leaving it. The reasons for it: - A page may be *login* protected. - A page may be *permission* protected, i.e.: you may be logged in but your permissions may not be enough to access a specific section. - A page may allow navigation away from it after confirming that changes to a text field don't have to be saved. - A page may ask you if you really want to log-out. Although it was not mentioned before, the definitions above from the :doc:`/tutorial/tourer/index` already contained an entering guard. Let's recall it .. code-block:: python ... from .auth_guard_service import AuthGuard ... routes = [ { 'path': 'compose', 'component': ComposeMessageComponent, 'outlet': 'popup' }, { 'path': 'disaster-center', 'load_children': [DiasterCenterModule] }, { 'path': 'admin', 'load_children': [AdminModule] 'can_activate', [AuthGuard], }, {'path': '', 'redirect_to': '/superpyroes', 'path_match': 'full'}, {'path': '*', 'component': PageNotFoundComponent}, ] where the key is:: ... from .auth_guard_service import AuthGuard ... { 'path': 'admin', 'load_children': [AdminModule] 'can_load', [AuthGuard], }, Entering the ``'admin'`` path (and with it the entire ``AdminModule``) is guarded by ``AuthGuard``. Let's have a look to see what it is doing .. code-block:: python from anpylar import AuthGuard class AuthGuard(AuthGuard): def can_activate(self, route): return self.check_login(route.path) def can_activate_child(self, route): return self.can_activate(route) def check_login(self, path): if self.auth_service.is_logged: return True self.auth_service.redir_path = path self.router.route_to('/login', session_id=1234567890) return False It can be easily spotted that the key method is:: def can_activate(self, route): return self.check_login(route.path) The main activation control: - ``def can_activate(self, route)`` Takes a *route* parameter, which contains the details of the actual route to be activated and - Returns ``True`` if the route can be activated - Returns ``False`` if the route cannot be activated In this case the work is delegated to another method of the *guard* which: - Will redirect to ``/login`` with a ``session_id`` if the user was not previously logged in. Route De-Activation ******************* Once a route is active, the component has taken over and that's why deactivation is delegated to the *Component*. Using code from the advance router .. code-block:: python class DisasterDetailComponent(Component): ... def can_deactivate(self): if not self.edit_did or self.selected.name == self.edit_name: return True # dialog_service is in the main module return self.dialog_service.confirm('Discard changes?') Skipping most of the code from ``DisasterDetailComponent`` allows us to focus on the ``can_deactivate`` method. It has to: - Return ``True`` if one can navigate away - Return ``False`` if one cannot navigate away It may: - Return an *Observable* which will finally generate either ``True`` of ``False`` This is exactly what's being made in this case with:: return self.dialog_service.confirm('Discard changes?') A dialog is presented to the user and to avoid blocking things, the result will be piped through an *Observable* The details of subscribing to the observable and using the result to carry on with navigation are fully managed by the platform. Routing in components ********************* The ``routerlink`` directive ============================ In HTML Code ------------ Routing links can be directly specified in html code (or generated html code). The main application component ``AppComponent`` in the *Tour of Pyroes* has this html code .. code-block:: html Using the ``routerlink`` (or ``routerLink`` as you please) attribute means that the specified path will be passed to the routing engine for processing. If you had done it the usual way: .. code-block:: html