23

What is the correct (canonical) way to display current time in angular 4 change detection system?

The problem is as follows: current time changes constantly, each moment, by definition. But it is not detectable by Angular 4 change detection system. For this reason, in my opinion, it's necessary to explicitly call ChangeDetectorRef::detectChanges. However, in the process of calling this method, current time naturally changes its value. This leads to ExpressionChangedAfterItHasBeenCheckedError. In the following example (see live) this error appears withing a few seconds after loading the page:

import { Component, ChangeDetectorRef } from '@angular/core';
@Component({
    selector: 'my-app',
    template: `{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />
        {{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />
        {{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />
        {{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />
        {{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />
        {{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />
        {{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />
        {{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}<br />{{ now }}`
})
export class AppComponent {
    get now() : string { return Date(); }
    constructor(cd: ChangeDetectorRef) {
        setInterval(function() { cd.detectChanges(); }, 1);
    }
}
BOPOHOB
  • 525
  • 1
  • 4
  • 13

7 Answers7

48

First of all you don't need to call ChangeDetectorRef.detectChanges() inside your interval, because angular is using Zone.js which monkey patches the browsers setInteral method with its own. Therefore angular is well aware of the changes happening during an interval.

You should set the time inside your interval like this:

import { Component } from '@angular/core';
@Component({
    selector: 'my-app',
    template: `{{ now }}`
})
export class AppComponent {
    public now: Date = new Date();

    constructor() {
        setInterval(() => {
          this.now = new Date();
        }, 1);
    }
}

But you shouldn't update the time on such a high rate, because it would lead to poor perfomance, because everytime the date updates angular performs a changedetection on the component tree.

If you want to update the DOM at a very high rate, you should use runOutsideAngular from NgZone and update the DOM manually using the Renderer2.

For example:

@Component({
  selector: 'my-counter',
  template: '<span #counter></span>'
})
class CounterComponent implements OnChange {
  public count: number = 0;

  @ViewChild('counter')
  public myCounter: ElementRef;

  constructor(private zone: NgZone, private renderer: Renderer2) {
    this.zone.runOutsideAngular(() => {
      setInterval(() => {
        this.renderer.setProperty(this.myCounter.nativeElement, 'textContent', ++this.count);
      }, 1);
    });
  }
}
Jouke
  • 330
  • 4
  • 17
cyr_x
  • 13,987
  • 2
  • 32
  • 46
  • 1
    Thank you very much for such detailed explanation, it clarified a lot. I wanted to make sure that this example is a fundamental Angular flaw: why do we have to explicitly call `runOutsideAngular` and explicitly set intervals manually, outside Angular change detection system? Does it not contradict the framework paradigm? – BOPOHOB Oct 04 '17 at 10:45
  • 1
    That's not a flaw. The change-detection isn't ment for updates at such a high rate, that's why angular has the `NgZone` service which provides a way to manually do calculations or update the view without invoking the change detection algorithm. – cyr_x Oct 04 '17 at 11:10
  • Solution from first example may cause ExpressionChangedAfterItHasBeenCheckedError, i.e. there is no way to show time without direct element access. There is flaw i mean... Correct solution go outside angular template. In google solution from here https://github.com/urish/angular2-moment it's wrapped by pipe which looks much better – BOPOHOB Oct 04 '17 at 11:33
  • No the first example won't cause an `ExpressionChangedAfterItHasBeenCheckedError` because it's running inside the zone, it only has a huge performance impact at that rate. The only thing is that if the component is using the `ChangeDetectionStrategy.OnPush` angular won't pick up the changes. – cyr_x Oct 04 '17 at 11:38
14

As per Angular documentation on the DatePipe Link, Date.now() can be used.

import { Component } from '@angular/core';
@Component({
    selector: 'my-app',
    template: `{{ now | date:'HH:mm:ss'}}`
})
export class AppComponent {
    now:number;

    constructor() {
        setInterval(() => {
          this.now = Date.now();
        }, 1);
    }
}
mrOak
  • 193
  • 3
  • 12
6

Actually, you don't need any library for this simple task. If you are doing in your angular project with angular 6+ then import formatDate from the common package and pass other data. Here is a sample:

import { Component } from '@angular/core';
import {formatDate } from '@angular/common';
@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  today= new Date();
  todaysDataTime = '';

  constructor() {
    this.todaysDataTime = formatDate(this.today, 'dd-MM-yyyy hh:mm:ss a', 'en-US', '+0530');
  }
}

Here is a stackblitz link, you can edit here: https://stackblitz.com/edit/angular-h63y8e

Oliver
  • 561
  • 1
  • 8
  • 14
5
today: number = Date.now();

constructor() {
    setInterval(() => {this.today = Date.now()}, 1);
}

If you want to format this on html page, use it like this:

<p>{{today | date:'fullDate'}} {{today | date:'h:mm:ss a'}}</p>

I'm adding one more point, which I was finding and could come closer to my solution because of this answer, so for another one who's seeking a similar thing, I'm adding this too :)

If you want to get [Year: Month :date :hours: minutes with am/pm ] format with all numbers (ex : 12 instead of the string "December"), simply use the following

<p>{{today | date:'y:M:d:h:mm a'}}</p>
Mario Petrovic
  • 7,500
  • 14
  • 42
  • 62
Jinto Antony
  • 458
  • 8
  • 26
3
CurrentTime: any;
  constructor() {
    setInterval(() => {
      this.CurrentTime = new Date().getHours() + ':' + new Date().getMinutes() + ':'+  new Date().getSeconds()}, 1);
  }
Kai
  • 2,529
  • 1
  • 15
  • 24
0

in angular 2+ if we are using reactive form

<input formControlName="scheduleTime" type="time"  class="form-control ">


this.orderForm.controls.scheduleTime.setValue(new Date().toString().split(' ')[4]);

using above code we will display current time in time input

BAPUJI
  • 11
  • 2
-2

Use arrow function.

constructor(cd: ChangeDetectorRef){
    setInterval(() => { cd.detectChanges();  }, 1);
  }
phani indra
  • 243
  • 1
  • 10