refactor(aio): simplify and document the NavigationService
This commit is contained in:
parent
b017fbe48e
commit
55189b1b85
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user