refactor(aio): simplify and document the NavigationService

This commit is contained in:
Peter Bacon Darwin 2017-03-06 14:55:03 +00:00 committed by Igor Minar
parent b017fbe48e
commit 55189b1b85

View File

@ -16,26 +16,34 @@ export interface NavigationViews {
[name: string]: NavigationNode[]; [name: string]: NavigationNode[];
} }
export interface NavigationMap {
[url: string]: NavigationMapItem;
}
export interface NavigationMapItem {
node: NavigationNode;
parents: NavigationNode[];
}
const navigationPath = 'content/navigation.json'; const navigationPath = 'content/navigation.json';
@Injectable() @Injectable()
export class NavigationService { export class NavigationService {
navigationViews = this.fetchNavigation(); /**
selectedNodes = this.getActiveNodes(); * An observable collection of NavigationNode trees, which can be used to render navigational menus
*/
navigationViews = this.fetchNavigationViews();
/**
* An observable array of nodes that indicate which nodes in the `navigationViews` match the current URL location
*/
selectedNodes = this.getSelectedNodes();
constructor(private http: Http, private location: LocationService, private logger: Logger) { } constructor(private http: Http, private location: LocationService, private logger: Logger) { }
private fetchNavigation(): Observable<NavigationViews> { /**
* Get an observable that fetches the `NavigationViews` from the server.
* We create an observable by calling `http.get` but then publish it to share the result
* among multiple subscribers, without triggering new requests.
* We use `publishLast` because once the http request is complete the request observable completes.
* If you use `publish` here then the completed request observable will cause the subscribed observables to complete too.
* We `connect` to the published observable to trigger the request immediately.
* We could use `.refCount` here but then if the subscribers went from 1 -> 0 -> 1 then you would get
* another request to the server.
* We are not storing the subscription from connecting as we do not expect this service to be destroyed.
*/
private fetchNavigationViews(): Observable<NavigationViews> {
const navigationViews = this.http.get(navigationPath) const navigationViews = this.http.get(navigationPath)
.map(res => res.json() as NavigationViews) .map(res => res.json() as NavigationViews)
.publishLast(); .publishLast();
@ -43,34 +51,41 @@ export class NavigationService {
return navigationViews; return navigationViews;
} }
private getActiveNodes() { /**
const currentMapItem = combineLatest( * Get an observable that will list the nodes that are currently selected
this.navigationViews.map(this.computeNavMap), * We use `publishReplay(1)` because otherwise subscribers will have to wait until the next
* URL change before they receive an emission.
* See above for discussion of using `connect`.
*/
private getSelectedNodes() {
const selectedNodes = combineLatest(
this.navigationViews.map(this.computeUrlToNodesMap),
this.location.currentUrl, this.location.currentUrl,
(navMap, url) => navMap[url]); (navMap, url) => navMap[url] || [])
const activeNodes = currentMapItem .publishReplay(1);
.map(item => item ? [item.node, ...item.parents] : []) selectedNodes.connect();
.publishReplay(); return selectedNodes;
activeNodes.connect();
return activeNodes;
} }
private computeNavMap(navigation: NavigationViews): NavigationMap { /**
const navMap: NavigationMap = {}; * Compute a mapping from URL to an array of nodes, where the first node in the array
Object.keys(navigation).forEach(key => navigation[key].forEach(node => walkNodes(node, null))); * is the one that matches the URL and the rest are the ancestors of that node.
*
* @param navigation A collection of navigation nodes that are to be mapped
*/
private computeUrlToNodesMap(navigation: NavigationViews) {
const navMap = {};
Object.keys(navigation).forEach(key => navigation[key].forEach(node => walkNodes(node)));
return navMap; return navMap;
function walkNodes(node: NavigationNode, parent: NavigationMapItem | null) { function walkNodes(node: NavigationNode, ancestors: NavigationNode[] = []) {
const item: NavigationMapItem = { node, parents: [] }; const nodes = [node, ...ancestors];
if (parent) {
item.parents = [parent.node, ...parent.parents];
}
if (node.url) { if (node.url) {
// only map to this item if it has a url associated with it // only map to this node if it has a url associated with it
navMap[node.url] = item; navMap[node.url] = nodes;
} }
if (node.children) { if (node.children) {
node.children.forEach(child => walkNodes(child, item)); node.children.forEach(child => walkNodes(child, nodes));
} }
} }
} }