-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(directives): Added linkActive directive
- Loading branch information
1 parent
9bc9dcd
commit 7750329
Showing
2 changed files
with
217 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import {Directive, Input, Query, QueryList, Renderer, ElementRef, AfterViewInit, OnDestroy} from 'angular2/core'; | ||
import { LinkTo } from './link-to'; | ||
import { Location } from './location'; | ||
|
||
export interface ActiveOptions { | ||
exact: boolean; | ||
} | ||
|
||
/** | ||
* The LinkActive directive toggles classes on elements that contain an active linkTo directive | ||
* | ||
* <a linkActive="active" linkTo="/home/page">Home Page</a> | ||
* <ol> | ||
* <li linkActive="active" *ngFor="var link of links"> | ||
* <a [linkTo]="'/link/' + link.id">{{ link.title }}</a> | ||
* </li> | ||
* </ol> | ||
*/ | ||
@Directive({ selector: '[linkActive]' }) | ||
export class LinkActive implements AfterViewInit, OnDestroy { | ||
@Input('linkActive') activeClass: string; | ||
@Input() activeOptions: ActiveOptions = { exact: true }; | ||
private _sub: any; | ||
|
||
constructor( | ||
@Query(LinkTo) public links:QueryList<LinkTo>, | ||
public element: ElementRef, | ||
public location$: Location, | ||
public renderer: Renderer | ||
) {} | ||
|
||
ngAfterViewInit() { | ||
this._sub = this.location$ | ||
.map(({path}) => this.location$.prepareExternalUrl(path)) | ||
.subscribe(path => { | ||
this.checkActive(path); | ||
}); | ||
} | ||
|
||
checkActive(path) { | ||
let active = this.links.reduce((active, current) => { | ||
let [href, query] = current.linkHref.split('?'); | ||
|
||
if (this.activeOptions.exact) { | ||
return active ? active : href === path; | ||
} else { | ||
return active ? active : path.startsWith(href); | ||
} | ||
}, false); | ||
|
||
let activeClasses = this.activeClass.split(' '); | ||
activeClasses.forEach((activeClass) => { | ||
this.renderer.setElementClass(this.element.nativeElement, activeClass, active); | ||
}); | ||
} | ||
|
||
ngOnDestroy() { | ||
if (this._sub) { | ||
this._sub.unsubscribe(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import { | ||
describe, | ||
beforeEach, | ||
beforeEachProviders, | ||
it, | ||
iit, | ||
TestComponentBuilder, | ||
injectAsync, | ||
expect | ||
} from 'angular2/testing'; | ||
import { | ||
Component, | ||
provide | ||
} from 'angular2/core'; | ||
import { LinkTo } from '../lib/link-to'; | ||
import { LinkActive } from '../lib/link-active'; | ||
import { LOCATION_PROVIDERS, Location } from '../lib/location'; | ||
import { SpyLocation } from 'angular2/router/testing'; | ||
import { Observable } from 'rxjs/Observable'; | ||
import { BehaviorSubject } from 'rxjs/subject/BehaviorSubject'; | ||
import { LocationStrategy } from 'angular2/src/router/location/location_strategy'; | ||
import { MockLocationStrategy } from 'angular2/src/mock/mock_location_strategy'; | ||
|
||
@Component({ | ||
selector: 'link-active-test', | ||
template: '', | ||
directives: [LinkTo, LinkActive] | ||
}) | ||
class TestComponent{} | ||
|
||
const compile = (tcb: TestComponentBuilder, template: string = '') => { | ||
return tcb | ||
.overrideTemplate(TestComponent, template) | ||
.createAsync(TestComponent); | ||
}; | ||
|
||
describe('Link Active', () => { | ||
beforeEachProviders(() => [ | ||
LOCATION_PROVIDERS, | ||
provide(LocationStrategy, { useClass: MockLocationStrategy }) | ||
]); | ||
|
||
it('should be defined', () => { | ||
expect(LinkActive).toBeDefined(); | ||
}); | ||
|
||
it('should add the provided class to the active element', injectAsync([TestComponentBuilder, Location], (tcb, location$) => { | ||
location$.next({ | ||
path: '/page' | ||
}); | ||
|
||
return compile(tcb, '<a linkActive="active" linkTo="/page">Page</a>') | ||
.then((fixture) => { | ||
fixture.detectChanges(); | ||
let compiled = fixture.debugElement.nativeElement; | ||
let link: Element = compiled.querySelector('a'); | ||
|
||
expect(link.getAttribute('class')).toEqual('active'); | ||
}); | ||
})); | ||
|
||
it('should support multiple classes on the active element', injectAsync([TestComponentBuilder, Location], (tcb, location$) => { | ||
location$.next({ | ||
path: '/page' | ||
}); | ||
|
||
return compile(tcb, '<a linkActive="active orange" linkTo="/page">Page</a>') | ||
.then((fixture) => { | ||
fixture.detectChanges(); | ||
let compiled = fixture.debugElement.nativeElement; | ||
let link: Element = compiled.querySelector('a'); | ||
|
||
expect(link.getAttribute('class')).toEqual('active orange'); | ||
}); | ||
})); | ||
|
||
it('should add the provided class to a child element', injectAsync([TestComponentBuilder, Location], (tcb, location$) => { | ||
location$.next({ | ||
path: '/page' | ||
}); | ||
|
||
return compile(tcb, ` | ||
<div linkActive="active"> | ||
<a linkTo="/page">Page</a> | ||
</div> | ||
`) | ||
.then((fixture) => { | ||
fixture.detectChanges(); | ||
let compiled = fixture.debugElement.nativeElement; | ||
let parentElement: Element = compiled.querySelector('div'); | ||
|
||
expect(parentElement.getAttribute('class')).toEqual('active'); | ||
}); | ||
})); | ||
|
||
it('should add the provided class to a parent element with one active child element', injectAsync([TestComponentBuilder, Location], (tcb, location$) => { | ||
location$.next({ | ||
path: '/page2' | ||
}); | ||
|
||
return compile(tcb, ` | ||
<div linkActive="active"> | ||
<a linkTo="/page">Page</a><br> | ||
<a linkTo="/page2">Page</a><br> | ||
<a linkTo="/page3">Page</a><br> | ||
</div> | ||
`) | ||
.then((fixture) => { | ||
fixture.detectChanges(); | ||
let compiled = fixture.debugElement.nativeElement; | ||
let parentElement: Element = compiled.querySelector('div'); | ||
|
||
expect(parentElement.getAttribute('class')).toEqual('active'); | ||
}); | ||
})); | ||
|
||
it('should match parent/child elements when using non-exact match', injectAsync([TestComponentBuilder, Location], (tcb, location$) => { | ||
location$.next({ | ||
path: '/pages/page2' | ||
}); | ||
|
||
return compile(tcb, ` | ||
<a id="pages" linkActive="active" [activeOptions]="{ exact: false }" linkTo="/pages">Pages</a><br> | ||
<a id="page2" linkActive="active" linkTo="/pages/page2">Page 2</a><br> | ||
`) | ||
.then((fixture) => { | ||
fixture.detectChanges(); | ||
let compiled = fixture.debugElement.nativeElement; | ||
let pagesLink: Element = compiled.querySelector('#pages'); | ||
let page2Link: Element = compiled.querySelector('#page2'); | ||
|
||
expect(pagesLink.getAttribute('class')).toEqual('active'); | ||
expect(page2Link.getAttribute('class')).toEqual('active'); | ||
}); | ||
})); | ||
|
||
it('should only check path for active link', injectAsync([TestComponentBuilder, Location], (tcb, location$) => { | ||
return compile(tcb, ` | ||
<a linkActive="active" linkTo="/pages/page2" [queryParams]="{ search: 'criteria' }">Page 2</a><br> | ||
`) | ||
.then((fixture) => { | ||
fixture.detectChanges(); | ||
let compiled = fixture.debugElement.nativeElement; | ||
let link: Element = compiled.querySelector('a'); | ||
|
||
location$.next({ | ||
path: '/pages/page2' | ||
}); | ||
|
||
fixture.detectChanges(); | ||
expect(link.getAttribute('href')).toEqual('/pages/page2?search=criteria'); | ||
expect(link.getAttribute('class')).toEqual('active'); | ||
}); | ||
})); | ||
}); |