I'm having trouble with my service. I have a service wich get a JSON from the HTTP module and fill a class 'olders' with it. But when I call my service, it doesn't do anything and my class olders is still undifined...
And if I make a *ngFor directive in the yearbook.component.html, I just get nothing...
I think my problem came from my service, but I don't know where is the error...
Even if i put the console.log inside of the subscribe
my yearbook.component :
import {Component} from '#angular/core'
import {Olders} from './olders'
import {OldersService} from './olders.service'
#Component({
moduleId: module.id,
selector: 'yearbook',
templateUrl: 'yearbook.component.html',
})
export class YearbookComponent {
olders : Olders[];
constructor(
private _oldersService : OldersService
){}
ngOnInit() {
this._oldersService.getOldersfromAPI()
.subscribe( res => {this.olders = res,
console.log(this.olders)},
err => console.error(err.status)
);
}
}
and the olders.service :
import {Injectable} from '#angular/core'
import {Http} from '#angular/http'
import {Olders} from './olders'
import 'rxjs/add/operator/map'
import 'rxjs/add/operator/do'
#Injectable ()
export class OldersService {
constructor(private _http:Http) {
}
getOldersfromAPI(){
return this._http.get('../CDN/olders.json')
.do(x => console.log(x))
.map(olders => {olders = olders.json()});
}
}
Thanks in advance for all your answers guys
You are missing a return statement in your mapping:
.map(olders => {olders = olders.json()});
should be:
.map(olders => { return olders = olders.json()});
Related
I have written an implementation to obtain data from API calls. However, while testing the functionality I am getting the following errors even before writing any meaningful test cases:
TypeError: Cannot read property 'subscribe' of undefined
at DataComponent.ngOnInit (http://localhost:9876/_karma_webpack_/webpack:/src/app/dashboard/job/job.component.ts:48:10)
at callHook (http://localhost:9876/_karma_webpack_/webpack:/node_modules/#angular/core/__ivy_ngcc__/fesm2015/core.js:3405:1)
at callHooks (http://localhost:9876/_karma_webpack_/webpack:/node_modules/#angular/core/__ivy_ngcc__/fesm2015/core.js:3375:1)
at executeInitAndCheckHooks (http://localhost:9876/_karma_webpack_/webpack:/node_modules/#angular/core/__ivy_ngcc__/fesm2015/core.js:3327:1)
at refreshView (http://localhost:9876/_karma_webpack_/webpack:/node_modules/#angular/core/__ivy_ngcc__/fesm2015/core.js:8573:1)
at renderComponentOrTemplate (http://localhost:9876/_karma_webpack_/webpack:/node_modules/#angular/core/__ivy_ngcc__/fesm2015/core.js:8672:1)
at tickRootContext (http://localhost:9876/_karma_webpack_/webpack:/node_modules/#angular/core/__ivy_ngcc__/fesm2015/core.js:9885:1)
at detectChangesInRootView (http://localhost:9876/_karma_webpack_/webpack:/node_modules/#angular/core/__ivy_ngcc__/fesm2015/core.js:9910:1)
at RootViewRef.detectChanges (http://localhost:9876/_karma_webpack_/webpack:/node_modules/#angular/core/__ivy_ngcc__/fesm2015/core.js:10320:1)
at ComponentFixture._tick (http://localhost:9876/_karma_webpack_/webpack:/node_modules/#angular/core/__ivy_ngcc__/fesm2015/testing.js:243:1)
I'm not sure what I'm missing, any help would be appreciated.
I looked into the following to get some understanding:
Why am I getting a "Failed: Cannot read property 'subscribe' of undefined" while running tests? -> the solution did not work in my case
karma TypeError "Cannot read property 'subscribe' of undefined"
and some other references.
Here are my code files. I have removed certain parts of my code that I felt were not relevant to the issue at hand.
dataService.ts code:
import { Injectable } from '#angular/core';
import { HttpClient, HttpErrorResponse } from '#angular/common/http';
import { IData } from '../model/data.model';
import { Observable, throwError } from 'rxjs';
import {catchError, tap} from 'rxjs/operators';
#Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private http: HttpClient) { }
public getData(): Observable<IData[]>{
return this.http.get<IData[]>('url')
.pipe(
tap(data => console.log('Data Received')),
catchError(this.handleError)
);
}
private handleError(err: HttpErrorResponse){
//handle error code
}
}
component I am testing -> data.component.ts code:
import { Component, OnInit, Input} from '#angular/core';
import { IData } from '../../model/data.model';
import {DataService} from '../../service/data.service';
#Component({
selector: 'app-job',
templateUrl: './job.component.html',
styleUrls: ['./job.component.css']
})
export class JobComponent implements OnInit {
#Input() appId: string;
jobs: IData[] = [];
constructor( private dataService: DataService ) {}
ngOnInit(): void {
this.dataService.getData().subscribe({
next: data => {
this.data = data;
},
error: err => this.errorMessage = err
});
}
//other implementation
}
Testing file : data.spec.ts:
import { ComponentFixture, TestBed } from '#angular/core/testing';
import { DataComponent } from './job.component';
import { IData } from 'src/app/model/job.model';
import { DataService } from 'src/app/service/job-data.service';
import { of, Observable } from 'rxjs';
import { NO_ERRORS_SCHEMA } from '#angular/core';
describe('DataComponent', () => {
let component: DataComponent;
let fixture: ComponentFixture<DataComponent>;
let mockDataService ;
let JOBS: IData[];
beforeEach(async () => {
mockJobDataService = jasmine.createSpyObj(['getData']);
await TestBed.configureTestingModule({
declarations: [ DataComponent ],
providers: [
{provide: DataService, useValue: mockDataService}
]
})
.compileComponents()
.then(() => {
DATA = [{id: 'CJH'}];
fixture = TestBed.createComponent(DataComponent);
component = fixture.componentInstance;
fixture.detectChanges(); //updates bindings
});
});
it('should return true',() => {
expect(true).toBe(true);
})
});
When I remove fixture.detectChanges(), the error gets removed. But in my understanding the test cases should work even if I use this call anywhere in my test.
You are creating a spy object with mockJobDataService = jasmine.createSpyObj(['getData']); and are registering it correctly with this {provide: DataService, useValue: mockDataService}. At this point, your component should get created correctly with your fake service injected into it. The problem is that you aren't setting up the expected method call on your fake service.
Something like the following should do the trick as long as you do it before you call fixture.detectChanges(). Since you don't include the shape of the IJob interface, I can't tell you exactly, but I have made it work with typescript through casting it(({} as IJob)).
import { of } from 'rxjs';
mockJobDataService.getData.and.returnValue(of([({} as IJob)]));
This tells jasmine that anything using this fake data service, upon calling the getData method, the return from that method should be of([({} as IJob)])(this is an observable of type IJob array).
As you move forward and write tests that actually are testing your component, you will probably want to move the mocked method and the 'detectChanges' call into each test so that you can provide different fake data for every test.
For learning purposes I am trying to get datas from a json fake API and add "hey" to all titles before returning an Observable. So far I can display the data if I don't use Map and even while using map if I console.log my variable it says Observable but it does not display in my template.
<div class="col-6" *ngIf="courseDatas$ | async as courses else NoData">
<div class="card" *ngFor="let course of courses">
<div class="card-body">
<span><strong>User ID is : </strong>{{course.userId}}</span><br>
<span><strong>Title is : </strong>{{course.title}}</span><br>
<span><strong>Body is : </strong>{{course.body}}</span><br>
<span><strong>ID is : </strong>{{course.id}}</span>
</div>
<ng-template #NoData>No Data Available</ng-template>
</div>
</div>
App component :
import {Component, OnInit} from '#angular/core';
import {PostsService} from "./posts.service";
import {Observable} from "rxjs";
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
courseDatas$ : Observable<any>;
constructor(private posts : PostsService){}
ngOnInit(){
this.courseDatas$ = this.posts.getData();
}
}
posts Service :
import { Injectable } from '#angular/core'
import {HttpClient} from "#angular/common/http";
import {Observable} from "rxjs";
import {map} from "rxjs/operators";
#Injectable({
providedIn: 'root'
})
export class PostsService {
private postURL: string = 'https://jsonplaceholder.typicode.com/posts';
constructor(private http: HttpClient) {}
getData(): Observable<any> {
return this.http.get(this.postURL).pipe(
map(data => {
for (let datas of (data as Array<any>)){
datas.title = datas.title + "Hey";
}
}),
);
}
So, if I don't use the map operator in my getData method in my service everything displays properly. If I use the map operator if I console.log coursesDatas$ in App.Component the console says Observable so I don't understand why it does not work with my async pipe in the template. Also, if I use console.log(datas.title) inside my map operator it does log every titles with Hey at the end.
map should return something to mutate current property, in your case I guess you should return the data
getData(): Observable<any> {
return this.http.get(this.postURL).pipe(
map(data => {
for (let datas of (data as Array<any>)){
datas.title = datas.title + "Hey";
}
return data;
}),
);
}
by the way you can use Array.prototype's map instead of for loop too, to mutate your data
getData(): Observable<any> {
return this.http.get(this.postURL).pipe(
map(data => data.map(d => (d.title = d.title +"Hey", d));
}),
);
}
note that if curly braces are missing in arrow function, it will return automatically
I'm 9 hours a day on Angular trying to make some little projects mainly with services. Today, I'm trying to make a service that loops on data's fetching and the components update themselves according to new data. I've like 6 components using the service and the standard way to do it makes 6 times more requests that only one component does.
I heard about IntervalObservable but I don't know how to implement it on the component side. (And maybe I failed in the service too ...)
Here is some code.
app.module.ts :
import { FormsModule } from '#angular/forms';
import { NgModule } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
import { HttpModule } from '#angular/http';
import { AppComponent } from './app.component';
import { ROUTING } from './app.routes';
import {HardwareService} from "./services/hardware.service";
import {AfficheurComponent} from "./components/hardware/afficheur.component";
import {HardwareListComponent} from "./views/hardwarelist/hardwarelist.component";
#NgModule({
imports: [ BrowserModule, ROUTING, HttpModule, FormsModule],
declarations: [
AppComponent,
AfficheurComponent,
HardwareListComponent
],
bootstrap: [ AppComponent ],
providers: [ HardwareService ]
})
export class AppModule { }
hardware.service.ts :
import { Injectable } from '#angular/core';
import { Headers, Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/interval'
#Injectable()
export class HardwareService{
private apiUrl = 'http://10.72.23.11:5000'; // URL to web API
constructor (private http: Http) {}
getHardware(){
return Observable.interval(5000)
.flatMap(() => {
return this.http.get(this.apiUrl)
.map(this.extractData)
.catch(this.handleError);
});
}
private extractData(res: Response) {
let body = res.json();
return body || { };
}
private handleError (error: Response | any) {
// In a real-world app, you might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
}
afficheur.component.ts :
import { Component } from '#angular/core';
import {HardwareService} from "../../services/hardware.service";
#Component({
selector: 'afficheur',
templateUrl: 'app/components/hardware/afficheur.component.html'
})
export class AfficheurComponent{
public state: Boolean;
constructor(private service: HardwareService){
this.service
.getHardware()
.subscribe(data => (console.log(data), this.state = data.afficheur),
error => console.log(error),
() => console.log('Get etat afficheur complete'))
}
}
I took the information about IntervalObservable here (SO thread)
As always, I hope you'll be able to help me find my way through this problem :).
ERROR TypeError: Observable_1.Observable.interval is not a function
Regards, Jérémy.
(PS: English is not my mother language, don't hesitate to tell me if i told something you don't understand)
The solution would look something like:
// create an observable which fetch the data at intervals of 1 second
this._data$ = Observable
.timer(0, 1000)
.switchMap(() => this.getData())
// if an error is encountered then retry after 3 seconds
.retryWhen(errors$ => {
errors$.subscribe(error => this.logError(error));
return errors$.delay(3000);
})
.share();
timer(0, 1000) - produce the first value after 0ms and then at intervals of 1 second. Using interval(1000) instead is ok but the first value will come with a delay of 1 second.
switchMap(() => this.getData()) - switch to the observable provided by the callback which queries the actual resource
retryWhen(...) - if an error is encountered then logs the error and then retries
share() - shares a single subscription among the subscribers. This has the effect of calling getData() only once, instead of calling it for as many subscribers we might have.
Example - emit current dates, when getData() is called 5th time in a row then an error is thrown in order to test also the error situation.
Here is the working Plunker.
HardwareService
import { Injectable } from '#angular/core';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/timer';
import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/retry';
import 'rxjs/add/operator/retryWhen';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/share';
import 'rxjs/add/operator/switchMap';
import {Subject} from 'rxjs/Subject';
#Injectable()
export class HardwareService {
private _fetchCount = 0;
private _fetchCount$ = new Subject<number>();
private _data$: Observable<Date>;
public get data$(): Observable<Date> {
return this._data$;
}
public get fetchCount$(): Observable<number> {
return this._fetchCount$;
}
constructor() {
// create an observable which fetch the data at intervals of 1 second
this._data$ = Observable
.timer(0, 1000)
.switchMap(() => this.getData())
// if an error is encountered then retry after 3 seconds
.retryWhen(errors$ => {
errors$.subscribe(error => this.logError(error));
return errors$.delay(3000);
})
.share();
}
private logError(error) {
console.warn(new Date().toISOString() + ' :: ' + error.message);
}
private getData(): Observable<Date> {
this._fetchCount++;
this._fetchCount$.next(this._fetchCount);
// from time to time create an error, after 300ms
if (this._fetchCount % 5 === 0) {
return Observable.timer(300).switchMap(() => Observable.throw(new Error('Error happens once in a while')));
}
// this will return current Date after 300ms
return Observable.timer(300).switchMap(() => Observable.of(new Date()));
}
}
AfficheurComponent
import {Component, Input, OnInit} from '#angular/core';
import {HardwareService} from '../services/hardware.service';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/map';
#Component({
selector: 'app-afficheur',
templateUrl: './afficheur.component.html',
styleUrls: ['./afficheur.component.css']
})
export class AfficheurComponent implements OnInit {
#Input()
public label: string;
public data$: Observable<string>;
constructor(private hardwareService: HardwareService) {
this.data$ = hardwareService.data$.map(item => this.label + ' - ' + item.toISOString());
}
ngOnInit() {
}
}
AfficheurComponent template
<div style="margin-top: 10px;">{{ data$ | async }}</div>
Usage
<app-afficheur label="afficheur 1"></app-afficheur>
<app-afficheur label="afficheur 2"></app-afficheur>
<app-afficheur label="afficheur 3"></app-afficheur>
<app-afficheur label="afficheur 4"></app-afficheur>
<app-afficheur label="afficheur 5"></app-afficheur>
<app-afficheur label="afficheur 6"></app-afficheur>
<div style="margin-top: 10px">
Times called: {{ hardwareService.fetchCount$ | async }}
</div>
After reading almost everything I found about observables, I still don't understand pretty well how they work.
I am doing the http request here:
import { Component, OnInit, Injectable } from '#angular/core';
import { Http, Response, Headers, RequestOptions } from '#angular/http';
import { NavController } from 'ionic-angular';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
#Injectable()
#Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
webs: any;
getWebs(): any{
return this.http.get( 'here the url' )
.map((res: Response) => res.json());
}
constructor(public navCtrl: NavController, private http: Http) {}
ngOnInit(){
this.getWebs().subscribe(response => {
this.webs = response;
console.log(this.webs);
});
}
}
On the console, this.webs is correctly printed. That means, the get request ist working fine and I am retrieving the object I want. That is a normal JSON object.
The problem is, on the view, if I try to print some property of the object (the same properties I see on the console) like that
{{ webs.name }}
I get the whole time that error:
Error in ./HomePage class HomePage - caused by: Cannot read property 'name' of undefined
That was sooo easy with Angular 1 :( I already read a lot of tutorials but I can't find any answer to my problem.
Thanks for your help.
The view is shown before the http response is returned.
{{webs?.name}}
should work.
Or do this.webs=getWebs()
and {{webs.name | async}}
It should be something
this.getWebs().then((webs) => {
webs.subscribe(response => {
this.webs = response;
resolve(webs);
console.log(this.webs);
});
})
so after you getWebs do this.This is untested code but you get the logic.
You are calling before you get data.
ngOnInit(){
return new Promise(resolve => {
this.http.get('webs.json')
.map(res => res.json())
.subscribe(webs => {
this.webs = webs;
resolve(this.webs);
});
});
}
I want to create a service to get data from .json file once and share it to multiple subscribers. But now with my solution number of requests to get data from .json file equals to a number of a subscibers for my service.
getconfig.service.ts
import {Injectable} from 'angular2/core';
import {Http, Response} from "angular2/http";
#Injectable()
export class ConfigService {
config: any;
http: Http;
constructor(http: Http) {
this.http = http;
console.log('Inside service');
this.config = this.http.get('/config.json');
}
}
robotui.component.ts
...
import {ConnectionService} from '../services/connection.service';
#Component({
...
providers: [HTTP_PROVIDERS, ConfigService, ConnectionService]
...
})
constructor(private _configService: ConfigService) {
this._configService.config.subscribe((observer) => {
console.log('Subscribe in RobotUI component', JSON.parse(observer._body));
});
}
actual.photo.component.ts
import {Component} from 'angular2/core';
import {ConfigService} from '../services/getconfig.service';
#Component({
...
providers: [ConfigService]
})
export class ActualPhotoComponent {
constructor(private _configService: ConfigService) {
this._configService.config.subscribe((observer) => {
console.log('Subscribe in ActualPhoto component', JSON.parse(observer._body));
});
}
}
When i run it in my console i see:
So, there is get request for each subscibers. I want a solution when i get config.json file only once, save this info in a service and share it with multiple subscibers.
That's because
#Component({
...
providers: [ConfigService] //<--- this creates service instance per component
})
To share data among controllers/components and to create single instance only, you have to inject your service into bootstrap function.
import {ConfigService } from './path to service';
bootstrap('AppCompoent',[configService]) //<----Inject here it will create a single instance only
In subscribing component,
robotui.component.ts
...
import {ConfigService} from '../services/getconfig.service'; //<----- Note this line here....
import {ConnectionService} from '../services/connection.service';
#Component({
...
... // No providers needed anymore
...
})
constructor(private _configService: ConfigService) {
this._configService.config.subscribe((observer) => {
console.log('Subscribe in RobotUI component', JSON.parse(observer._body));
});
}
actual.photo.component.ts
import {Component} from 'angular2/core';
import {ConfigService} from '../services/getconfig.service';
#Component({
...
... // No providers needed anymore...
})
export class ActualPhotoComponent {
constructor(private _configService: ConfigService) {
this._configService.config.subscribe((observer) => {
console.log('Subscribe in ActualPhoto component', JSON.parse(observer._body));
});
}
}
This is what you should do.