.. | ||
README.md |
Navigation refactoring
Introduction
Navigation code undertook complete rewrite. Previous navigation served for multiple purposes. These purposes were so tight together that it created several limitations for future enhancements of the framework. New implementation splits the code to several more or less independent components with given responsibilities. First part of this document describes old issues, second new implementation.
Glossary
- navigation: covers whole functionality of changing pages, updating of visual representation and hash.
- menu: only the visual representation on the page
- hash: part of the URL after
#
sign - router: component which matches hashes to facets
- facet: 'page' of the application
Problems of previous implementation
Global state
The change of facet state was done by changing hash part of URL. The change was handled by navigation, it caused the same chain of commands as showing a different page. Basically a facet told navigation to tell it that it needs to change it state, which is redundant. The only thing needed is to announce: ``I'm changing my state''. Navigation can then just update hash.
The process was as follows:
IPA.nav.push_state(state)
IPA.nav.update()
entity.display(facet_node)
- check if to reload facet
facet.show()
- several calls of
IPA.nav.get_state(key)
- do the action
Page change was the same with some additional steps.
Using global values also prevents using more facets on a one page. Such feature is not required yet but it is an unnecessary limitation which can be easily avoided.
Entity structure
Menu specification and UI structure required an entity to be specified for every page. entity.display(facet_node)
served for switching facets. The display
method was basically doing part of a work of application controller. An artificial entity would have to be created for pages without any entity.
Fixed dirty check
IPA.nav.push_state(state)
method contained a code which checks facet if it has some unsaved changes (is dirty) and displays a ``dirty dialog''. The type of the dialog was set in the method and therefore it was the same for each facet. It would create limitations for non-CRUD facets.
DOM representation bound with rest of the functionality
This was the biggest issue. DOM representation (menu) was required for switching facets. Navigation used a concept of tabs. A tab was a node in navigation tree structure. Each leaf tab represented an entity or entity's facet. So one couldn't have a working navigation without existing menu or a facet without corresponding menu item.
Difficult extensibility
It was very difficult to add item on particular position. The tree-like structure of tabs was hard-coded and initialized on init. Additional changes weren't supported. Navigation didn't offer any method which would add new item on certain position. Partial update of menu wasn't possible. Recreation wasn't tested.
New implementation
Rewrite of the navigation addresses all mentioned issues. The old IPA.navigation
class was split into server smaller ones with limited responsibilities:
navigation.menu_spec
navigation.Menu
navigation.Router
Application_controller
widgets.App
widgets.Menu
Menu
Menu is implemented in two classes: navigation.Menu
and widgets.Menu
.
navigation.Menu
Is a data model of a menu. It contains object store of menu items. It provides array of currently selected items and events which can be used for observation of changes.
New items can be added by simple method.
widgets.Menu
Is the HTML representation of a menu - a widget. It uses navigation.Menu
as it's data model. It listens to data model's selected
event to update selection state and observers data.models object store to reflect it's changes. Hence, new items are immediately rendered and removed are destroyed.
Menu offers item-select
event. Other components should listed to it and do appropriate work.
Menu widget doesn't change it's data model in any way. It just observes.
Router
navigation.Router
is applications router. It is an extension of dojo router. It adds a support for facets. It provides API for route registration, hash generation and update, and navigation to facets.
IMPORTANT: Facets widgets and other parts of application shouldn't use router directly. They should use navigation proxy(navigation
) instead.
Route registration
Router has register_route(routes, handler)
method for registering of routes. It's a wrapper of Dojo's router's register
method. The difference is that this method can register multiple routes for one handler and that the handler is bound to router object so it can has it reference.
Standard implementation has handler for entity pages and for standalone pages. Standalone pages are not supported yet because facet register (a map of facets) is not implemented yet.
Extensions can register their own routes+handlers to support new types of facets/hash representations.
Router maintains each route registration so they can be deleted if needed.
Route evaluation
Evaluation is done by dojo router component. When it matches a route, it calls corresponded handler.
Handlers and showing a facet
Route handlers receive dojo router's event argument objects. Handler can inspect old hash, new hash and state pulled from the new hash.
Handler should decode a facet and it's state, set the state to the facet and call show_facet(facet)
. It publishes facet-show
event. This tells the application that we should change the facet but router doesn't care how it's done.
Handler should always change if ignore_next
flag isn't set. It can use check_clear_ignore
which does this check and cleans the flag. When the flag is set the handler shouldn't do anything. The flag is mainly used when updating hash of already displayed facet. User can then bookmark the facet state.
Navigating to a facet
It's a opposite operation of hash change handling and facet show. Router has two build-in methods: navigate_to_facet
and navigate_to_entity_facet
. Their purpose is to create hash which can be matched by some route and then call navigate_to_hash(hash, facet)
.
navigate_to_hash
Tells application "I want to change facet" by raising facet-change
global event. Listeners can inspect associated facet and hash, then set navigation.canceled
property if they want to prevent the change. They should do it synchronously. When somebody cancels the change, navigation notifies it by raising facet-change-canceled
global event. Otherwise the hash is updated.
Updating hash
Hash can be updated in two cases: navigating to a facet and updating a facet state. The difference is in setting ignore_next
property. The former sets it, latter doesn't.
Navigation proxy
Implemented as singleton in .navigation
module. It's purpose is to offer interface for navigating between pages to facet, widgets and other components.
Consumers don't have to worry about internal implementation => they are not bound to specific router implementation.
Exposed methods:
show
show_entity
show_default
Check code for arguments documentation.
Extensions can add new methods.
Application controller
Application controller (AC) ties all the components together. It basically contains most of the integration logic as navigation did before but it delegates most of the actions to other components.
Application initialization
AC initialization is done in 3 phases: app-init
, metadata
, profile
. Creation of AC instance and phase registration is done in ./app
module.
app-init phase
- creates AC instance
- create menu store
- creates router
- creates app widget
- bounds menu widget with menu store
- registers handlers for:
- click in menu widget (
item-select
event) - select change in menu data model (
selected
event) - profile view (app widget event)
- logout (app widget event)
facet-show
(router)facet-change
(router)facet-change-canceled
(router)
- click in menu widget (
- subscribes to global events (topics):
phase-error
- renders app widget
metadata phase
basically calls IPA.init
to get metadata and profile information.
profile phase chooses menu based on identity information obtained in metadata phase. Adds menu items to menu.
Starts the router. Start of the router causes hash evaluation. A facet is displayed when hash is specified. When no facet is displayed, AC navigates to profile's default page.
phase error: When some phase fail, error content is displayed. At the time we can't count that everything is initialized so tools for displaying the error are very limited - no metadata, no profile, possibly no app widget.
Facet changing
AC listens to facet-change
event. It compares current and new facet and asks old one if it is dirty, when it is, AC obtains dirty dialog from current facet and displays it. Hence, facets can choose how the dirty dialog would look like. AC prevents the change when told by the dialog.
Facet showing
AC hides old facets and shows new one. If new doesn't have container AC sets it one and registers listener for facet's facet-state-change
event. At this point it also tries to identify menu item with the facet and select it. Unlike the previous implementation, it also works when no menu item is matched.
Menu clicks
On menu click AC tries to match facet with menu item and navigate to it.
Menu item is not selected because the change can be prevented. Therefore selection is performed in facet show handler.
Facet state changing
AC listens to facet-state-change
event. AC updates hash when displayed facet changes its state.
Multiple menu levels
AC expect that the menu will have 2-3 menu levels. Current used CSS doesn't automatically handle 3rd level. Hence, AC observes select state and sets special class on content node to inform CSS.
Further generalization
As a framework class AC should be further generalized, mostly to get rid of IPA specific functions (app widget and its handlers, profiles,...).
How does it all work together?
Now, when everything is described, we can proceed to method calls chains examples of most common use cases. It will explain what operations are executed for certain use cases.
Loading Web UI
It just goes through app-init
, metadata
, profiles
phases. At the end of profile phase a facet is selected based on hash, if there is no hash, default facet is selected. The difference is only lack of navigate_to_xxx
call in the former case. Details will be described in following chapter.
Navigation to a page
Initiated by:
// navigation is './navigation' module
var state = { key1: val1, key2: val2 };
navigation.show(target_facet, state);
- proxy gets reference to navigation of current AC:
nav
- proxy calls
nav.navigate_to_facet(facet, state);
navigate_to_facet
creates hash, callsnavigate_to_hash(facet, hash)
navigate_to_hash
ask app if it can change facet by raisingfacet-change
event- AC will responds to it and ask current facet if it's dirty, if so:
a. navigation is canceled. Must be canceled because it has to be synchronous and it can't wait for user input.
b.
facet-change-cancelled
event is raised, no build in listeners for it c. dirty dialog is displayed d. user selects action e. on confirmation AC callsnavigate_to_hash
with same params to proceed with the change - hash is updated
- hash changed event is raised and then processed by router
- router matches hash to handler and calls handler
- handler decodes facet and state from hash
- handler sets state to facet, it will cause appropriate actions in facet
- handler raises
facet-show
event facet-show
event is caught by AC (handler:on_facet_show
)- AC matches facet to menu item (
this._find_menu_item(facet)
) - AC selects menu item (
this.menu.select(menu_item)
) - menu store raises
selected
event a. menu widget redraw itself according to selection b. AC adds style for third level menu if needed (handler:on_menu_select
) - AC set container for facet if needed (usually for the first time)
- AC hides current facet (
current_facet.hide()
) - AC shows new facet (
facet.show()
)- facet clears and refreshes when
needs_update()
is true (might be set by as a result offacet.set_state(state)
call in hash handler
- facet clears and refreshes when
Click on menu item
- user clicks on menu item
- raises
item-select
event of menu widget - AC catches it
- AC traverse menu items to get first one with facet or entity set
- AC calls
navigate_to_entity_facet
ornavigate_to_facet
based on the result of previous operation - rest is same as in previous use case
Update of facet state
- facet updates its state
- facet publishes
facet-state-change
event. - AC catches it and calls
navigation.create_hash
and thennavigation.update_hash
- facet internally responds to the change
As you can see it just updates the hash but no navigation is done. This feature makes facet independent on navigation breaks one-facet displayed limitation (there can be facet containing more facets).
Set hash by hand in a browser when facet is dirty
Basically the same as navigation to a page, only from hash is updated step. This implementation, nor the previous one, doesn't check dirty state of currently displayed facet. Hence, facet is changed in both cases.
Open Questions
- Should facet state update be moved from navigation to app controller?
- Add extensibility point to Menu Store for supporting initialization of different types of menu items? Extract entity & facet logic to it.