6

Given a list of items (think of it as a chat with multiple messages), I'd like to use to display the relative time (e.g. since creation) for each item.

Each item has its own component and relative time is displayed using the following code:

get timeFromNow(): string {
  return moment(this.pageLoaded).fromNow(); // pageLoaded is a JS Date
}

The issue is that the component won't update the display of relative time, since the original input (pageLoaded in the example above) won't change.

My question is: is there a best practice for dealing with such a scenario? Currently, I'm using markForCheck of ChangeDetectorRef to trigger re-rendering. However, I'm not sure if this is a good method performance-wise.

I've created a simple demo on Stackblitz. In the demo, I use the aforementioned markForCheck to trigger the update.

tilo
  • 14,009
  • 6
  • 68
  • 85
  • Your StackBlitz seems to work? The value is updated to `a minute ago` after the 44 seconds. – user184994 Jul 01 '18 at 08:23
  • Correct! The Stackblitz demo uses the aforementioned method of utilizing manual change detection. If you'd remove this, the display would not update (added a comment in the demo). But: is this a good way of handling the issue? Or is there a better / more performant way? – tilo Jul 01 '18 at 08:26
  • The only other option would be to make the ChangeDetectionStrategy default, but that would probably be _worse_ performance wise. I think it may be worth reducing the interval duration though, checking every second is not needed, maybe just check every minute? Depends how accurate it needs to be – user184994 Jul 01 '18 at 08:55

2 Answers2

10

From a technical point of view you have a good solution. If you want a better design then enter the world of reactive programming.

Instead of starting a check manually you can use a stream and let Angular do the work:

export class MomentComponent implements OnInit  {
  pageLoaded: Moment;
  timeFromNow: Observable<string>;

  ngOnInit() {
    this.pageLoaded = moment(new Date());

    this.timeFromNow = interval(1000).pipe(
                         map(() => this.pageLoaded.fromNow()),
                         distinctUntilChanged()
                       );
  }
}

interval works like setInterval in your example: It emits a value every 1000ms. Then we replace this value by a time string. That's it. Every second a new time string is emitted. I added distinctUntilChanged() so that a new value is only emitted when it's different from the old one.

In your template you can consume the stream using the async pipe:

template: `Page loaded: <span class="relativeTime">{{ timeFromNow | async}}</span>`

The HTML will be updated when a new time string is emitted. No unnecessary checks and calculations. As a bonus by using the async pipe the timer is cancelled automatically when the component is destroyed and you don't create a memory leak.

You can check the forked demo.

a better oliver
  • 26,330
  • 2
  • 58
  • 66
  • 3
    This is a pretty nice solution, thanks! You may want to add `startWith(this.pageLoaded.fromNow())` to the pipe in order to avoid a blank value for the first second. – tilo Jul 01 '18 at 13:08
4

A even better solution is to use timer(0, 1000) instead of interval(1000). This will immediately fire, and then every second.

See http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html#static-method-timer

Eric Aya
  • 69,473
  • 35
  • 181
  • 253