Skip to content

Commit

Permalink
feat: add pipe
Browse files Browse the repository at this point in the history
  • Loading branch information
ihym committed May 25, 2018
1 parent 95ca44e commit fe58c8d
Show file tree
Hide file tree
Showing 7 changed files with 297 additions and 5 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class MyIntl extends TimeagoIntl {
@NgModule({
imports: [
BrowserModule,
Timeago.forRoot({
TimeagoModule.forRoot({
intl: { provide: TimeagoIntl, useClass: MyIntl },
formatter: { provide: TimeagoFormatter, useClass: TimeagoCustomFormatter },
})
Expand Down Expand Up @@ -143,11 +143,17 @@ export class AppComponent {
You can also customize the language strings or provide your own.


#### 2. Use the directive:
#### 2. Use the pipe or the directive:

This is how you do it with the **pipe**:
```html
<div>{{2671200000 | timeago:live}}</div>
```
And in your component define live (`true` by default).

This is how you use the **directive**:
```html
<div timeago [date]="2671200000" [live]="true"></div>
<div timeago [date]="2671200000" [live]="live"></div>
```

## API
Expand Down
2 changes: 1 addition & 1 deletion demo/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
</mat-card-header>
<mat-card-content>
<div class="clearfix w-100">
<strong class="float-left" timeago [date]="date" [live]="live"></strong>
<strong>{{date | timeago:live}}</strong>
<div class="float-right">
<mat-checkbox color="primary" [(ngModel)]="live">Live</mat-checkbox>
</div>
Expand Down
2 changes: 1 addition & 1 deletion demo/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import { Component, ChangeDetectionStrategy } from '@angular/core';
templateUrl: './app.component.html',
})
export class AppComponent {
date = Date.now() - 50000;
date = Date.now() - 58000;
live = true;
}
1 change: 1 addition & 0 deletions lib/src/public_api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './timeago.directive';
export * from './timeago.pipe';
export * from './timeago.intl';
export * from './timeago.clock';
export * from './timeago.formatter';
Expand Down
3 changes: 3 additions & 0 deletions lib/src/timeago.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NgModule, ModuleWithProviders, Provider } from '@angular/core';
import { TimeagoDirective } from './timeago.directive';
import { TimeagoPipe } from './timeago.pipe';
import { TimeagoIntl } from './timeago.intl';
import { TimeagoClock, TimeagoDefaultClock } from './timeago.clock';
import { TimeagoFormatter, TimeagoDefaultFormatter } from './timeago.formatter';
Expand All @@ -13,9 +14,11 @@ export interface TimeagoModuleConfig {
@NgModule({
declarations: [
TimeagoDirective,
TimeagoPipe,
],
exports: [
TimeagoDirective,
TimeagoPipe,
],
})
export class TimeagoModule {
Expand Down
92 changes: 92 additions & 0 deletions lib/src/timeago.pipe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {
Injectable,
OnDestroy,
Pipe,
PipeTransform,
Optional,
ChangeDetectorRef
} from '@angular/core';
import { Subscription, ReplaySubject } from 'rxjs';
import { TimeagoClock } from './timeago.clock';
import { TimeagoFormatter } from './timeago.formatter';
import { TimeagoIntl } from './timeago.intl';
import { isDefined, coerceBooleanProperty, dateParser } from './util';
import { filter, map } from 'rxjs/operators';

@Injectable()
@Pipe({
name: 'timeago',
pure: false, // required to update the value when stateChanges emits
})
export class TimeagoPipe implements PipeTransform, OnDestroy {
private intlSubscription: Subscription;
private clockSubscription: Subscription;

private date: number;
private value: string;
private live = true;

/**
* Emits on:
* - Input change
* - Intl change
* - Clock tick
*/
stateChanges = new ReplaySubject<void>();

constructor(@Optional() intl: TimeagoIntl,
cd: ChangeDetectorRef,
private formatter: TimeagoFormatter,
private clock: TimeagoClock) {
if (intl) {
this.intlSubscription = intl.changes.subscribe(() => this.stateChanges.next());
}
this.stateChanges.subscribe(() => {
this.value = formatter.format(this.date);
cd.markForCheck();
});
}

transform(date: number, ...args: any[]) {
const _date = dateParser(date).valueOf();
let _live: boolean;

_live = isDefined(args[0])
? coerceBooleanProperty(args[0])
: this.live;

if (this.date === _date && this.live === _live) {
return this.value;
}

this.date = _date;
this.live = _live;

if (this.date) {
if (this.clockSubscription) {
this.clockSubscription.unsubscribe();
this.clockSubscription = undefined;
}
this.clockSubscription = this.clock.tick(date)
.pipe(filter(() => this.live, this))
.subscribe(() => this.stateChanges.next());
this.stateChanges.next();
} else {
throw new SyntaxError(`Wrong parameter in TimeagoPipe. Expected a valid date, received: ${date}`);
}

return this.value;
}

ngOnDestroy() {
if (this.intlSubscription) {
this.intlSubscription.unsubscribe();
this.intlSubscription = undefined;
}
if (this.clockSubscription) {
this.clockSubscription.unsubscribe();
this.clockSubscription = undefined;
}
this.stateChanges.complete();
}
}
190 changes: 190 additions & 0 deletions tests/pipe.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Injectable, ViewChild, ElementRef } from '@angular/core';
import { TestBed, fakeAsync, tick, discardPeriodicTasks } from '@angular/core/testing';
import {
TimeagoModule,
TimeagoClock,
TimeagoFormatter,
TimeagoPipe,
TimeagoIntl,
TimeagoCustomFormatter,
IL10nsStrings
} from '../lib/src/public_api';

const strings: IL10nsStrings = {
prefixAgo: null,
prefixFromNow: null,
suffixAgo: 'ago',
suffixFromNow: 'from now',
second: '%d second',
seconds: '%d seconds',
minute: '%d minute',
minutes: '%d minutes',
hour: '%d hour',
hours: '%d hours',
day: 'a day',
days: '%d days',
month: '%d month',
months: '%d months',
year: '%d year',
years: '%d years',
wordSeparator: ' ',
};

class FakeChangeDetectorRef extends ChangeDetectorRef {
markForCheck(): void {
}

detach(): void {
}

detectChanges(): void {
}

checkNoChanges(): void {
}

reattach(): void {
}
}

@Injectable()
@Component({
selector: 'app-root',
template: `
<div #static>{{date | timeago:false}}</div>
<div #live>{{date | timeago:true}}</div>
<div #var>{{date | timeago:isLive}}</div>
`,
})
class AppComponent {
@ViewChild('static') static: ElementRef;
@ViewChild('live') live: ElementRef;
@ViewChild('var') var: ElementRef;

date = Date.now() - 1000;
isLive = false;
}

describe('TimeagoPipe', () => {
let clock: TimeagoClock;
let formatter: TimeagoFormatter;
let intl: TimeagoIntl;
let pipe: TimeagoPipe;
let ref: ChangeDetectorRef;
let date: number;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
TimeagoModule.forRoot({
formatter: { provide: TimeagoFormatter, useClass: TimeagoCustomFormatter }
})
],
providers: [TimeagoIntl],
declarations: [AppComponent]
});
date = Date.now() - 1000;
clock = TestBed.get(TimeagoClock);
formatter = TestBed.get(TimeagoFormatter);
intl = TestBed.get(TimeagoIntl);
intl.strings = { ...strings };
ref = new FakeChangeDetectorRef();
});

afterEach(() => {
clock = undefined;
formatter = undefined;
intl = undefined;
ref = undefined;
pipe = undefined;
});

it('is defined', () => {
pipe = new TimeagoPipe(intl, ref, formatter, clock);

expect(TimeagoPipe).toBeDefined();
expect(pipe).toBeDefined();
expect(pipe instanceof TimeagoPipe).toBeTruthy();
});

it('should render a formatted timestamp', () => {
pipe = new TimeagoPipe(intl, ref, formatter, clock);

expect(pipe.transform(date)).toEqual('1 second ago');
});

it('should call markForCheck when it formats a timestamp', () => {
pipe = new TimeagoPipe(intl, ref, formatter, clock);

spyOn(ref, 'markForCheck').and.callThrough();

pipe.transform(date);
expect(ref.markForCheck).toHaveBeenCalled();
});

it('should throw if you dont give a valid date', () => {
pipe = new TimeagoPipe(intl, ref, formatter, clock);

expect(() => {
pipe.transform(null);
}).toThrowError(`Wrong parameter in TimeagoPipe. Expected a valid date, received: null`);
});

it('should handle clock ticks properly based on input', fakeAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
expect(fixture.debugElement.componentInstance.static.nativeElement.innerHTML).toEqual('1 second ago');
expect(fixture.debugElement.componentInstance.live.nativeElement.innerHTML).toEqual('1 second ago');

tick(1000);

fixture.detectChanges();
expect(fixture.debugElement.componentInstance.static.nativeElement.innerHTML).toEqual('1 second ago');
expect(fixture.debugElement.componentInstance.live.nativeElement.innerHTML).toEqual('2 seconds ago');
discardPeriodicTasks();
}));

it('should listen to intl changes', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
expect(fixture.debugElement.componentInstance.static.nativeElement.innerHTML).toEqual('1 second ago');

intl.strings.second = '%d testSecond';
intl.changes.next();

fixture.detectChanges();
expect(fixture.debugElement.componentInstance.static.nativeElement.innerHTML).toEqual('1 testSecond ago');
});

it('should listen to date changes', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.componentInstance.date = Date.now() - 2000;
fixture.detectChanges();

expect(fixture.debugElement.componentInstance.static.nativeElement.innerHTML).toEqual('2 seconds ago');
});

it('should listen to live changes', fakeAsync(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();

expect(fixture.debugElement.componentInstance.var.nativeElement.innerHTML).toEqual('1 second ago');

tick(1000);
fixture.detectChanges();

expect(fixture.debugElement.componentInstance.var.nativeElement.innerHTML).toEqual('1 second ago');

fixture.componentInstance.isLive = true;
fixture.detectChanges();

expect(fixture.debugElement.componentInstance.var.nativeElement.innerHTML).toEqual('2 seconds ago');

tick(1000);
fixture.detectChanges();

expect(fixture.debugElement.componentInstance.var.nativeElement.innerHTML).toEqual('3 seconds ago');

discardPeriodicTasks();
}));
});

0 comments on commit fe58c8d

Please sign in to comment.