diff --git a/aio/src/app/navigation/navigation.service.ts b/aio/src/app/navigation/navigation.service.ts index fa74ea0408..de5d4a500a 100644 --- a/aio/src/app/navigation/navigation.service.ts +++ b/aio/src/app/navigation/navigation.service.ts @@ -16,26 +16,34 @@ export interface NavigationViews { [name: string]: NavigationNode[]; } -export interface NavigationMap { - [url: string]: NavigationMapItem; -} - -export interface NavigationMapItem { - node: NavigationNode; - parents: NavigationNode[]; -} - const navigationPath = 'content/navigation.json'; @Injectable() 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) { } - private fetchNavigation(): Observable { + /** + * 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 { const navigationViews = this.http.get(navigationPath) .map(res => res.json() as NavigationViews) .publishLast(); @@ -43,34 +51,41 @@ export class NavigationService { return navigationViews; } - private getActiveNodes() { - const currentMapItem = combineLatest( - this.navigationViews.map(this.computeNavMap), + /** + * Get an observable that will list the nodes that are currently selected + * 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, - (navMap, url) => navMap[url]); - const activeNodes = currentMapItem - .map(item => item ? [item.node, ...item.parents] : []) - .publishReplay(); - activeNodes.connect(); - return activeNodes; + (navMap, url) => navMap[url] || []) + .publishReplay(1); + selectedNodes.connect(); + return selectedNodes; } - private computeNavMap(navigation: NavigationViews): NavigationMap { - const navMap: NavigationMap = {}; - Object.keys(navigation).forEach(key => navigation[key].forEach(node => walkNodes(node, null))); + /** + * Compute a mapping from URL to an array of nodes, where the first node in the array + * 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; - function walkNodes(node: NavigationNode, parent: NavigationMapItem | null) { - const item: NavigationMapItem = { node, parents: [] }; - if (parent) { - item.parents = [parent.node, ...parent.parents]; - } + function walkNodes(node: NavigationNode, ancestors: NavigationNode[] = []) { + const nodes = [node, ...ancestors]; if (node.url) { - // only map to this item if it has a url associated with it - navMap[node.url] = item; + // only map to this node if it has a url associated with it + navMap[node.url] = nodes; } if (node.children) { - node.children.forEach(child => walkNodes(child, item)); + node.children.forEach(child => walkNodes(child, nodes)); } } }