feat(router): implement RouterLinkActive
This commit is contained in:
parent
2aa19fd078
commit
25560ed048
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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];
|
|
@ -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 {
|
||||
|
|
|
@ -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} = {};
|
||||
|
|
|
@ -658,6 +658,90 @@ describe("Integration", () => {
|
|||
})));
|
||||
});
|
||||
});
|
||||
|
||||
describe("routerActiveLink", () => {
|
||||
it("should set the class when the link is active (exact = true)",
|
||||
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
||||
const fixture = tcb.createFakeAsync(RootCmp);
|
||||
advance(fixture);
|
||||
|
||||
router.resetConfig([
|
||||
{ path: 'team/:id', component: TeamCmp, children: [
|
||||
{ path: 'link', component: DummyLinkCmp, children: [
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
{path: '', component: BlankCmp}
|
||||
] }
|
||||
] }
|
||||
]);
|
||||
|
||||
router.navigateByUrl('/team/22/link');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/team/22/link');
|
||||
|
||||
const native = fixture.debugElement.nativeElement.querySelector("a");
|
||||
expect(native.className).toEqual("active");
|
||||
|
||||
router.navigateByUrl('/team/22/link/simple');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/team/22/link/simple');
|
||||
expect(native.className).toEqual("");
|
||||
})));
|
||||
|
||||
it("should set the class on a parent element when the link is active (exact = true)",
|
||||
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
||||
const fixture = tcb.createFakeAsync(RootCmp);
|
||||
advance(fixture);
|
||||
|
||||
router.resetConfig([
|
||||
{ path: 'team/:id', component: TeamCmp, children: [
|
||||
{ path: 'link', component: DummyLinkWithParentCmp, children: [
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
{path: '', component: BlankCmp}
|
||||
] }
|
||||
] }
|
||||
]);
|
||||
|
||||
router.navigateByUrl('/team/22/link');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/team/22/link');
|
||||
|
||||
const native = fixture.debugElement.nativeElement.querySelector("link-parent");
|
||||
expect(native.className).toEqual("active");
|
||||
|
||||
router.navigateByUrl('/team/22/link/simple');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/team/22/link/simple');
|
||||
expect(native.className).toEqual("");
|
||||
})));
|
||||
|
||||
it("should set the class when the link is active (exact = false)",
|
||||
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
||||
const fixture = tcb.createFakeAsync(RootCmp);
|
||||
advance(fixture);
|
||||
|
||||
router.resetConfig([
|
||||
{ path: 'team/:id', component: TeamCmp, children: [
|
||||
{ path: 'link', component: DummyLinkCmp, children: [
|
||||
{path: 'simple', component: SimpleCmp},
|
||||
{path: '', component: BlankCmp}
|
||||
] }
|
||||
] }
|
||||
]);
|
||||
|
||||
router.navigateByUrl('/team/22/link;exact=false');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/team/22/link;exact=false');
|
||||
|
||||
const native = fixture.debugElement.nativeElement.querySelector("a");
|
||||
expect(native.className).toEqual("active");
|
||||
|
||||
router.navigateByUrl('/team/22/link/simple');
|
||||
advance(fixture);
|
||||
expect(location.path()).toEqual('/team/22/link/simple');
|
||||
expect(native.className).toEqual("active");
|
||||
})));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
function expectEvents(events:Event[], pairs: any[]) {
|
||||
|
@ -681,6 +765,27 @@ class StringLinkCmp {}
|
|||
})
|
||||
class AbsoluteLinkCmp {}
|
||||
|
||||
@Component({
|
||||
selector: 'link-cmp',
|
||||
template: `<router-outlet></router-outlet><a routerLinkActive="active" [routerLinkActiveOptions]="{exact: exact}" [routerLink]="['./']">link</a>`,
|
||||
directives: ROUTER_DIRECTIVES
|
||||
})
|
||||
class DummyLinkCmp {
|
||||
private exact: boolean;
|
||||
constructor(route: ActivatedRoute) {
|
||||
// convert 'false' into false
|
||||
this.exact = (<any>route.snapshot.params).exact !== 'false';
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'link-cmp',
|
||||
template: `<router-outlet></router-outlet><link-parent routerLinkActive="active"><a [routerLink]="['./']">link</a></link-parent>`,
|
||||
directives: ROUTER_DIRECTIVES
|
||||
})
|
||||
class DummyLinkWithParentCmp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'link-cmp',
|
||||
template: `<a [routerLink]="['../simple']">link</a>`,
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import {DefaultUrlSerializer} from '../src/url_serializer';
|
||||
import {UrlTree, containsTree} from '../src/url_tree';
|
||||
|
||||
describe('UrlTree', () => {
|
||||
const serializer = new DefaultUrlSerializer();
|
||||
|
||||
describe("containsTree", () => {
|
||||
describe("exact = true", () => {
|
||||
it("should return true when two tree are the same", () => {
|
||||
const url = "/one/(one//left:three)(right:four)";
|
||||
const t1 = serializer.parse(url);
|
||||
const t2 = serializer.parse(url);
|
||||
expect(containsTree(t1, t2, true)).toBe(true);
|
||||
expect(containsTree(t2, t1, true)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when paths are not the same", () => {
|
||||
const t1 = serializer.parse("/one/two(right:three)");
|
||||
const t2 = serializer.parse("/one/two2(right:three)");
|
||||
expect(containsTree(t1, t2, true)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when container has an extra child", () => {
|
||||
const t1 = serializer.parse("/one/two(right:three)");
|
||||
const t2 = serializer.parse("/one/two");
|
||||
expect(containsTree(t1, t2, true)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when containee has an extra child", () => {
|
||||
const t1 = serializer.parse("/one/two");
|
||||
const t2 = serializer.parse("/one/two(right:three)");
|
||||
expect(containsTree(t1, t2, true)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("exact = false", () => {
|
||||
it("should return true when containee is missing a segment", () => {
|
||||
const t1 = serializer.parse("/one/(two//left:three)(right:four)");
|
||||
const t2 = serializer.parse("/one/(two//left:three)");
|
||||
expect(containsTree(t1, t2, false)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true when containee is missing some paths", () => {
|
||||
const t1 = serializer.parse("/one/two/three");
|
||||
const t2 = serializer.parse("/one/two");
|
||||
expect(containsTree(t1, t2, false)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true container has its paths splitted into multiple segments", () => {
|
||||
const t1 = serializer.parse("/one/(two//left:three)");
|
||||
const t2 = serializer.parse("/one/two");
|
||||
expect(containsTree(t1, t2, false)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when containee has extra segments", () => {
|
||||
const t1 = serializer.parse("/one/two");
|
||||
const t2 = serializer.parse("/one/(two//left:three)");
|
||||
expect(containsTree(t1, t2, false)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return containee has segments that the container does not have", () => {
|
||||
const t1 = serializer.parse("/one/(two//left:three)");
|
||||
const t2 = serializer.parse("/one/(two//right:four)");
|
||||
expect(containsTree(t1, t2, false)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -35,6 +35,7 @@
|
|||
"src/interfaces.ts",
|
||||
"src/utils/tree.ts",
|
||||
"src/utils/collection.ts",
|
||||
"test/url_tree.spec.ts",
|
||||
"test/utils/tree.spec.ts",
|
||||
"test/url_serializer.spec.ts",
|
||||
"test/apply_redirects.spec.ts",
|
||||
|
|
Loading…
Reference in New Issue