Skip to content

Commit

Permalink
feat(directives): Added linkActive directive
Browse files Browse the repository at this point in the history
  • Loading branch information
Brandon Roberts authored and brandonroberts committed Apr 3, 2016
1 parent 9bc9dcd commit 7750329
Show file tree
Hide file tree
Showing 2 changed files with 217 additions and 0 deletions.
62 changes: 62 additions & 0 deletions lib/link-active.ts
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();
}
}
}
155 changes: 155 additions & 0 deletions spec/link-active.spec.ts
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');
});
}));
});

0 comments on commit 7750329

Please sign in to comment.