Trigger update of component view from service - No Provider for ChangeDetectorRef
Asked Answered
N

6

78

I would like to udpate my application view, triggered by events from service.

One of my services injects the ChangeDetectorRef. Compilation works, but I am getting an error in the browser when the App is bootstrapped: No provider for ChangeDetectorRef!.

I thought I needed to add it to my AppModule, but I can't find any documentation that suggests it is in a module that I can import there. I tried adding the class itself to the import array, but that caused an error. I also got an error trying to add it to the providers array in the module. Here is a simplified version of my service:

import {Injectable, ChangeDetectorRef } from '@angular/core';

@Injectable()
export class MyService {
  private count: number = 0;
  constructor(private ref: ChangeDetectorRef){}

  increment() {
    this.count++;
    this.ref.detectChanges();
}

And the app module:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';

import { MyService } from './my.service';

@NgModule({
  imports: [ BrowserModule ],
  declarations: [ AppComponent ],
  providers: [ MyService ],
  booststrap: [ AppComponent ]
})
export AppModule {}

UPDATE

I have since tried removing my use of ChangeDetectorRef, and I still have the same problem. I am guessing there is something wrong about how I updated my System JS config.

I originally created the app with angular-cli, and was trying to update Angular on my own since they had not updated that. With the final release of Angular 2.0.0 they have updated angular-cli to use the latest version of angular. So I am going to try using their upgrade procedures and hopefully that goes better.

Update 2

The webpack/angular-cli update went well. I have the app building now with Angular 2.0.0 in and angular-cli 1.0.0-beta14. I still get the same error in the browser. I tried removing the ChangeDetectorRef from the service, but I didn't really. I had it in two services. If I remove it from both services, then my app loads fine, and works well, except for where I was trying to use ChangeDetectorRef. Once I add it back in one of the files, the browser has complains about not being able to find a provider for it.

I tried importing it in my module, but it is not a module, so the transpiler complains. I tried listing it a a provider in my module, but it does not have a provide property, so the transpiler complains. similar issues if I try putting it in the declarations array.

Near answered 15/9, 2016 at 13:5 Comment(7)
Try adding ChangeDetectorRef in the declerations arrayBerey
Can you inject it into the component?Gasparo
I can imagine that you can't inject it into a service? Can you try injecting it into a component and than pass it to the service?Mckeehan
I have the same problem.Fromenty
Any solutions? have the same issue in ionic 2 RC0Clepsydra
In the end I ended up refactoring my code to make better use of the native change detection on the service so I didn't need the ChangeDetectorRef anymore. I did this by replacing objects instead of just updating values in the object. This is not necessarily ideal in every situation, but it worked for me.Near
It works if you add it inside a @component, not in a service.Knockwurst
L
82

ChangeDetectorRef is not option to use here. It is looking for changes in a given component and its children.

In your case It would be better to use ApplicationRef:

import {Injectable, ApplicationRef } from '@angular/core';

@Injectable()
export class MyService {
  private count: number = 0;
  constructor(private ref: ApplicationRef) {}

  increment() {
    this.count++;
    this.ref.tick();
  }
}

I checked this solution with Observables and it works without any problems:

import { ApplicationRef, Injectable } from '@angular/core';
import { Observable, ReplaySubject } from "rxjs/Rx";
import * as childProcess from 'child_process';

@Injectable()
export class Reader {

    private output: ReplaySubject<string> = new ReplaySubject<string>(0);

    constructor(private ref: ApplicationRef) {
        var readerProcess = childProcess.spawn('some-process');
        readerProcess.stdout.on('data', (data) => {
            this.output.next(data.toString());
            this.ref.tick();
        });
    }

    public getOutput():Observable<string> {
        return this.output;
    }
}

and here is a component which uses it:

import {Component} from '@angular/core';
import { ReplaySubject, Observable } from "rxjs/Rx";

import { Reader } from './reader/reader.service';

@Component({
    selector: 'app',
    template: `
    output:
    <div>{{output}}</div>
    `
})
export class App {

    public output: string;

    constructor(private reader: Reader) {}

    ngOnInit () {
        this.reader.getOutput().subscribe((val : string) => {
            this.output = val;
        });
    }
}
Leroy answered 16/10, 2016 at 21:28 Comment(1)
I want to ask one thing that can i use 'this.ref.tick();' inside observer subscribe() method?. if Use then it gives error 'ApplicationRef.tick is called recursively'Blaisdell
N
14

Another option if you want to trigger the change from the service but allow the components to control if, when, and how they respond is to set up subscriptions.

In your service, add an EventEmitter:

changeDetectionEmitter: EventEmitter<void> = new EventEmitter<void>();

When something happens in the service that might need to trigger a change detection cycle, simply emit:

this.changeDetectionEmitter.emit();

Now, in any components that use this service, if they need to listen for possible change triggers, they can subscribe and react appropriately:

this.myService.changeDetectionEmitter.subscribe(
    () => {
      this.cdRef.detectChanges();
    },
    (err) => {
      // handle errors...
    }
  );

I kinda like this approach because it leaves control of change detection with the components where it belongs, but allows me to centralize logic in the service. The service doesn't need to know anything about the UI, it's just notifying anyone who is listening that something changed.

Nomenclator answered 11/2, 2018 at 21:20 Comment(0)
H
2

I know im long overdue for this but I tried simply to pass the ChangeDetectorRef to the Service via the function being called.

//myservice
import { Injectable, ChangeDetectorRef } from '@angular/core';

@Injectable()
export class MyService {
  constructor(){}

  increment(ref: ChangeDetectorRef, output: number) {
    output++;
    ref.detectChanges();
}

The Component

import {Component} from '@angular/core';
import { MyService } from './myservice.service';

@Component({
    selector: 'app',
    template: `
    output:
    <div>{{output}}</div>
    `
})
export class App {

    public output: number;

    constructor(public ref: ChangeDetectorRef, private myService: MyService) {}

    ngOnInit () {
       this.myService.increment(this.ref,output)
    }
}
Hamlin answered 19/8, 2019 at 12:46 Comment(0)
O
0

in the latest versions is part of the core now, so just make a reference to the core but using the variable needed and should do trick

import { TestBed } from '@angular/core/testing';
import { ChangeDetectorRef } from '@angular/core';
import { MyService } from './my.service';

describe('MyService', () => {
    beforeEach(() => TestBed.configureTestingModule({
        providers: [ChangeDetectorRef] // <--- LOOK AT ME I'M A PROVIDER!
    }));

    it('should be created', () => {
        const service: MyService = TestBed.get(MyService);
        expect(service).toBeTruthy();
    });
});

Hope helps

Override answered 17/6, 2017 at 11:44 Comment(0)
W
-1

To use ApplicationRef is a good solution. I have a few more solutions:

  1. Using setTimeout

    @Injectable()
    export class MyService {
      private count: number = 0;
      constructor(){}
    
      increment() {
        setTimeout(() => this.count++, 0);
      }
    }
    
  2. Using Promise

    @Injectable()
    export class MyService {
      private count: number = 0;
      constructor(){}
    
      increment() {
        Promise.resolve().then(() => this.count++);
      }
    }
    

Both solutions force Angular to trigger change detection, if you are using Zone.js for change detection - this is the default way and 99% of the apps use it.

Wampum answered 19/10, 2019 at 14:0 Comment(0)
A
-2

You need to add your service to providers on component level:

@Component({
  // ...
  providers: [MyService]
})
export class SomeComponent {
  // ...
}
Amygdala answered 16/8, 2020 at 22:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.