Skip to content

Instantly share code, notes, and snippets.

@JohannesRudolph
Created December 1, 2016 09:58
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JohannesRudolph/8e6de056d9e33353f940d9da9e6ffd82 to your computer and use it in GitHub Desktop.
Save JohannesRudolph/8e6de056d9e33353f940d9da9e6ffd82 to your computer and use it in GitHub Desktop.
Angular2 TimeAgo Pipe
import { fakeAsync, tick } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/interval';
import 'rxjs/add/operator/takeWhile';
import { TimeAgoPipe } from './time-ago.pipe';
import { WrappedValue } from '@angular/core';
// Learning
describe('Observable', () => {
it('standard Observable.interval does not continue after .takeWhile', fakeAsync(() => {
let n = 0;
let m = 0;
Observable
.interval(100)
.do(_ => n++)
.takeWhile(_ => n < 3)
.subscribe(_ => m++);
tick(600);
expect(n).toBe(3);
expect(m).toBe(2);
// observe that there's also no "periodic timers still left in the queue" error
}));
});
describe('TimeAgoPipe', () => {
describe('on Date', () => {
let pipe: TimeAgoPipe;
let ref: any;
let start: Date;
let updated: number;
beforeEach(() => {
start = new Date();
updated = 0;
ref = {
markForCheck: () => updated++
};
pipe = new TimeAgoPipe(ref);
});
function safePipeSpec(spec: () => void) {
return fakeAsync(() => {
try {
spec();
} finally {
pipe.ngOnDestroy(); // destroy pipe
tick(30000); // ensure the periodic timer has a chance to cleanup after itself
}
});
}
describe('transform', () => {
it('should update value every second', safePipeSpec(() => {
pipe.transform(start);
expect(updated).toBe(1);
tick(1000);
expect(updated).toBe(2);
}));
it('should properly format values', safePipeSpec(() => {
// monkey-patch pipe
let elapsed = 0;
function elapse(x) {
elapsed += x;
tick(x);
};
(<any>pipe).now = () => new Date(start.getTime() + elapsed);
function unwrap(x: string | WrappedValue) {
return x instanceof String ? x : x.wrapped;
}
pipe.transform(start);
elapse(1000); // +1s = 1s
expect(unwrap(pipe.transform(start))).toBe('1s ago');
elapse(10000); // +10s = 11s
expect(unwrap(pipe.transform(start))).toBe('11s ago');
elapse(50000); // +50s = 61s
expect(unwrap(pipe.transform(start))).toBe('1m ago');
elapse(60 * 60 * 1000); // +1h = 1h61s(60s*60m*1000ms)
expect(unwrap(pipe.transform(start))).toBe('1h ago');
elapse(24 * 60 * 60 * 1000); // +24h = 25h61s
expect(unwrap(pipe.transform(start))).toBe('1d ago');
}));
});
describe('ngOnDestroy', () => {
it('should do nothing when no subscription', () => {
expect(() => pipe.ngOnDestroy()).not.toThrow();
});
it('should ensure no more change detection cycle triggered', fakeAsync(() => {
pipe.transform(start);
expect(updated).toBe(1);
pipe.ngOnDestroy();
tick(1000);
expect(updated).toBe(1);
}));
});
});
describe('on null', () => {
it('should return empty string', () => {
let pipe = new TimeAgoPipe(null);
expect(pipe.transform(null)).toBe('');
});
});
describe('on other types', () => {
it('should throw', () => {
let pipe = new TimeAgoPipe(null);
expect(() => pipe.transform(<any>'some bogus object')).toThrowError();
});
});
});
import { OnDestroy, ChangeDetectorRef, Pipe, PipeTransform } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/interval';
import 'rxjs/add/operator/repeatWhen';
import 'rxjs/add/operator/startWith';
import 'rxjs/add/operator/takeWhile';
@Pipe({
name: 'timeAgo',
pure: false
})
export class TimeAgoPipe implements PipeTransform, OnDestroy {
private readonly async: AsyncPipe;
private isDestroyed = false;
private value: Date;
private timer: Observable<string>;
constructor(ref: ChangeDetectorRef) {
this.async = new AsyncPipe(ref);
}
public transform(obj: any, ...args: any[]): any {
if (obj == null) {
return '';
}
if (!(obj instanceof Date)) {
throw new Error('TimeAgoPipe works only with Dates');
}
this.value = obj;
if (!this.timer) {
this.timer = this.getObservable();
}
return this.async.transform(this.timer);
}
public now(): Date {
return new Date();
}
public ngOnDestroy() {
this.isDestroyed = true;
// on next interval, will complete
}
private getObservable() {
return Observable
.of(1)
.repeatWhen(notifications => {
// for each next raised by the source sequence, map it to the result of the returned observable
return notifications.flatMap((x, i) => {
const sleep = i < 60 ? 1000 : 30000;
return Observable.timer(sleep);
});
})
.takeWhile(_ => !this.isDestroyed)
.map((x, i) => this.elapsed());
};
private elapsed(): string {
let now = this.now().getTime();
// time since message was sent in seconds
let delta = (now - this.value.getTime()) / 1000;
// format string
if (delta < 60) { // sent in last minute
return `${Math.floor(delta)}s ago`;
} else if (delta < 3600) { // sent in last hour
return `${Math.floor(delta / 60)}m ago`;
} else if (delta < 86400) { // sent on last day
return `${Math.floor(delta / 3600)}h ago`;
} else { // sent more than one day ago
return `${Math.floor(delta / 86400)}d ago`;
}
}
}
@dorthrithil
Copy link

Great! To correct for timezones I used this:
let delta = (now - this.value.getTime()) / 1000 - this.value.getTimezoneOffset() * 60;

@jziggas
Copy link

jziggas commented Jun 28, 2019

How is ChangeDetectorRef available as a provider?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment