feat(router): implement RouterLinkActive

This commit is contained in:
vsavkin
2016-06-15 09:01:05 -07:00
parent 2aa19fd078
commit 25560ed048
8 changed files with 296 additions and 4 deletions
@@ -1,8 +1,9 @@
import {Directive, HostBinding, HostListener, Input, OnChanges} from '@angular/core';
import {Router} from '../router';
import {UrlTree} from '../url_tree';
import {ActivatedRoute} from '../router_state';
import {UrlTree} from '../url_tree';
/**
@@ -40,7 +41,7 @@ export class RouterLink implements OnChanges {
// the url displayed on the anchor element.
@HostBinding() href: string;
private urlTree: UrlTree;
urlTree: UrlTree;
/**
* @internal
@@ -0,0 +1,61 @@
import {AfterContentInit, ContentChildren, Directive, ElementRef, Input, OnChanges, OnDestroy, QueryList, Renderer} from '@angular/core';
import {Subscription} from 'rxjs/Subscription';
import {NavigationEnd, Router} from '../router';
import {containsTree} from '../url_tree';
import {RouterLink} from './router_link';
interface RouterLinkActiveOptions {
exact: boolean;
}
@Directive({selector: '[routerLinkActive]'})
export class RouterLinkActive implements OnChanges, OnDestroy, AfterContentInit {
@ContentChildren(RouterLink) private links: QueryList<RouterLink>;
private classes: string[] = [];
private subscription: Subscription;
@Input() private routerLinkActiveOptions: RouterLinkActiveOptions = {exact: true};
/**
* @internal
*/
constructor(private router: Router, private element: ElementRef, private renderer: Renderer) {
this.subscription = router.events.subscribe(s => {
if (s instanceof NavigationEnd) {
this.update();
}
});
}
ngAfterContentInit(): void {
this.links.changes.subscribe(s => this.update());
this.update();
}
@Input()
set routerLinkActive(data: string[]|string) {
if (Array.isArray(data)) {
this.classes = <any>data;
} else {
this.classes = data.split(' ');
}
}
ngOnChanges(changes: {}): any { this.update(); }
ngOnDestroy(): any { this.subscription.unsubscribe(); }
private update(): void {
if (!this.links || this.links.length === 0) return;
const currentUrlTree = this.router.parseUrl(this.router.url);
const isActive = this.links.reduce(
(res, link) =>
res || containsTree(currentUrlTree, link.urlTree, this.routerLinkActiveOptions.exact),
false);
this.classes.forEach(
c => this.renderer.setElementClass(this.element.nativeElement, c, isActive));
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
import {RouterLink} from './directives/router_link';
import {RouterLinkActive} from './directives/router_link_active';
import {RouterOutlet} from './directives/router_outlet';
export {ExtraOptions} from './common_router_providers';
@@ -12,4 +13,4 @@ export {PRIMARY_OUTLET, Params} from './shared';
export {DefaultUrlSerializer, UrlSerializer} from './url_serializer';
export {UrlPathWithParams, UrlTree} from './url_tree';
export const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink];
export const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink, RouterLinkActive];
+1 -1
View File
@@ -484,8 +484,8 @@ class ActivateRoutes {
{provide: ActivatedRoute, useValue: future},
{provide: RouterOutletMap, useValue: outletMap}
]);
outlet.activate(future._futureSnapshot._resolvedComponentFactory, future, resolved, outletMap);
advanceActivatedRoute(future);
outlet.activate(future._futureSnapshot._resolvedComponentFactory, future, resolved, outletMap);
}
private deactivateOutletAndItChildren(outlet: RouterOutlet): void {
+55
View File
@@ -6,6 +6,53 @@ export function createEmptyUrlTree() {
return new UrlTree(new UrlSegment([], {}), {}, null);
}
export function containsTree(container: UrlTree, containee: UrlTree, exact: boolean): boolean {
if (exact) {
return equalSegments(container.root, containee.root);
} else {
return containsSegment(container.root, containee.root);
}
}
function equalSegments(container: UrlSegment, containee: UrlSegment): boolean {
if (!equalPath(container.pathsWithParams, containee.pathsWithParams)) return false;
if (Object.keys(container.children).length !== Object.keys(containee.children).length)
return false;
for (let c in containee.children) {
if (!container.children[c]) return false;
if (!equalSegments(container.children[c], containee.children[c])) return false;
}
return true;
}
function containsSegment(container: UrlSegment, containee: UrlSegment): boolean {
return containsSegmentHelper(container, containee, containee.pathsWithParams);
}
function containsSegmentHelper(
container: UrlSegment, containee: UrlSegment, containeePaths: UrlPathWithParams[]): boolean {
if (container.pathsWithParams.length > containeePaths.length) {
const current = container.pathsWithParams.slice(0, containeePaths.length);
if (!equalPath(current, containeePaths)) return false;
if (Object.keys(containee.children).length > 0) return false;
return true;
} else if (container.pathsWithParams.length === containeePaths.length) {
if (!equalPath(container.pathsWithParams, containeePaths)) return false;
for (let c in containee.children) {
if (!container.children[c]) return false;
if (!containsSegment(container.children[c], containee.children[c])) return false;
}
return true;
} else {
const current = containeePaths.slice(0, container.pathsWithParams.length);
const next = containeePaths.slice(container.pathsWithParams.length);
if (!equalPath(container.pathsWithParams, current)) return false;
return containsSegmentHelper(container.children[PRIMARY_OUTLET], containee, next);
}
}
/**
* A URL in the tree form.
*/
@@ -44,6 +91,14 @@ export function equalPathsWithParams(a: UrlPathWithParams[], b: UrlPathWithParam
return true;
}
export function equalPath(a: UrlPathWithParams[], b: UrlPathWithParams[]): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; ++i) {
if (a[i].path !== b[i].path) return false;
}
return true;
}
export function mapChildren(segment: UrlSegment, fn: (v: UrlSegment, k: string) => UrlSegment):
{[name: string]: UrlSegment} {
const newChildren: {[name: string]: UrlSegment} = {};