From fe58c8d6d44062f3a2c7c1239d5550ba6da30775 Mon Sep 17 00:00:00 2001 From: Vasilis Diakomanolis Date: Fri, 25 May 2018 23:50:39 +0300 Subject: [PATCH] feat: add pipe --- README.md | 12 ++- demo/app/app.component.html | 2 +- demo/app/app.component.ts | 2 +- lib/src/public_api.ts | 1 + lib/src/timeago.module.ts | 3 + lib/src/timeago.pipe.ts | 92 +++++++++++++++++ tests/pipe.spec.ts | 190 ++++++++++++++++++++++++++++++++++++ 7 files changed, 297 insertions(+), 5 deletions(-) create mode 100644 lib/src/timeago.pipe.ts create mode 100644 tests/pipe.spec.ts diff --git a/README.md b/README.md index 563f496..ef38853 100644 --- a/README.md +++ b/README.md @@ -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 }, }) @@ -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 +
{{2671200000 | timeago:live}}
+``` +And in your component define live (`true` by default). This is how you use the **directive**: ```html -
+
``` ## API diff --git a/demo/app/app.component.html b/demo/app/app.component.html index fd9420d..248f688 100644 --- a/demo/app/app.component.html +++ b/demo/app/app.component.html @@ -14,7 +14,7 @@
- + {{date | timeago:live}}
Live
diff --git a/demo/app/app.component.ts b/demo/app/app.component.ts index 10ffc31..c6d9a9e 100644 --- a/demo/app/app.component.ts +++ b/demo/app/app.component.ts @@ -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; } diff --git a/lib/src/public_api.ts b/lib/src/public_api.ts index f38af36..341db6f 100644 --- a/lib/src/public_api.ts +++ b/lib/src/public_api.ts @@ -1,4 +1,5 @@ export * from './timeago.directive'; +export * from './timeago.pipe'; export * from './timeago.intl'; export * from './timeago.clock'; export * from './timeago.formatter'; diff --git a/lib/src/timeago.module.ts b/lib/src/timeago.module.ts index 083b0aa..0c3851f 100644 --- a/lib/src/timeago.module.ts +++ b/lib/src/timeago.module.ts @@ -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'; @@ -13,9 +14,11 @@ export interface TimeagoModuleConfig { @NgModule({ declarations: [ TimeagoDirective, + TimeagoPipe, ], exports: [ TimeagoDirective, + TimeagoPipe, ], }) export class TimeagoModule { diff --git a/lib/src/timeago.pipe.ts b/lib/src/timeago.pipe.ts new file mode 100644 index 0000000..1342b6c --- /dev/null +++ b/lib/src/timeago.pipe.ts @@ -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(); + + 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(); + } +} diff --git a/tests/pipe.spec.ts b/tests/pipe.spec.ts new file mode 100644 index 0000000..2e44568 --- /dev/null +++ b/tests/pipe.spec.ts @@ -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: ` +
{{date | timeago:false}}
+
{{date | timeago:true}}
+
{{date | timeago:isLive}}
+ `, +}) +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(); + })); +});