How do you map and use a JSON reponse that is a single object, rather than an array?
Recently, I started adding a new feature to a project I'm working on that should be taking a JSON response from an API and filling out a simple template with data from it. Shouldn't be difficult, right? Well, no... and yet, yes...
Mock version of the JSON response:
{
"id": 1,
"name": "Acaeris",
}
profile.service.ts
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Profile } from './profile';
/**
* This class provides the Profile service with methods to read profile data
*/
#Injectable()
export class ProfileService {
/**
* Creates a new ProfileService with the injected Http.
* #param {Http} http - The injected Http.
* #constructor
*/
constructor(private http: Http) {}
/**
* Returns an Observable for the HTTP GET request for the JSON resource.
* #return {Profile} The Observable for the HTTP request.
*/
get(): Observable<Profile> {
return this.http.get('assets/profile.json')
.map(res => <Profile>res.json())
.catch(this.handleError);
}
/**
* Handle HTTP error
*/
private handleError (error: any) {
let errMsg = (error.message) ? error.message :
error.status ? `${error.status} - ${error.statusText}` : 'Server error';
console.error(errMsg);
return Observable.throw(errMsg);
}
}
profile.component.ts
import { Component, OnInit } from '#angular/core';
import { ProfileService } from '../services/profile/profile.service';
import { Profile } from '../services/profile/profile';
/**
* This class represents the lazy loaded ProfileComponent
*/
#Component({
moduleId: module.id,
selector: 'sd-profile',
templateUrl: 'profile.component.html',
styleUrls: ['profile.component.css'],
})
export class ProfileComponent implements OnInit {
errorMessage: string;
profile: Profile;
/**
* Creates an instance of the ProfileComponent with the injected
* ProfileService
*
* #param {ProfileService} profileService - The injected ProfileService
*/
constructor(public profileService: ProfileService) {}
/**
* Get the profile data
*/
ngOnInit() {
this.getProfile();
}
/**
* Handles the profileService observable
*/
getProfile() {
this.profileService.get()
.subscribe(
data => this.profile = data,
error => this.errorMessage = <any>error
);
}
}
profile.ts
export interface Profile {
id: number;
name: string;
}
And I'm just trying to output it using {{profile.name}} but this ends up with the console showing a whole load of error messages and no output. If I try to check the contents of profile after it has loaded, it tells me it is undefined.
However, here's the confusing part. If I replace all the Profile references to Profile[], wrap the JSON in an array, add *ngFor="let p of profile" abd use {{p.name}} everything works fine. Unfortunately, in the actual finished application I would not have control of the JSON format. So what am I doing wrong when trying to handle it as a single object in comparison to handling as an array of objects?
Looks like at expression {{profile.name}} profile variable is undefined at page rendering moment. You can try either add some getter like this:
get profileName(): string { return this.profile ? this.profile.name ? ''; }
and use at template {{profileName}} or you can use ngIf at template like this:
<div *ngIf="profile">{{profile.name}}</div>
or shorter (as drewmoore suggested at comment below):
<div>{{profile?.name}}</div>
When you are working with array it is the same situation - at first rendering time array is undefined. ngFor handles this for you and renders nothing. When async operation of getting 'profile items' is complete - UI is rerendered again with correct values.
The mapfunction returns Observables which are a collection of elements. It basically work the same way as the map function for arrays.
Now to solve you can replace the Profile references by Profile[] and use {{profile[0].name}}.
Related
maybe someone can help me: I have created my first Angular package. The package itself has to provide different services to make HTTP requests to a server and return the results.
However, when importing the service, the web page stops rendering and I get an error message:
core.mjs:6484 ERROR Error: ASSERTION ERROR: token must be defined [Expected=> null != undefined <=Actual]
at throwError (core.mjs:326)
at assertDefined (core.mjs:322)
at bloomHashBitOrFactory (core.mjs:3591)
at getOrCreateInjectable (core.mjs:3379)
at Module.ɵɵdirectiveInject (core.mjs:14392)
at NodeInjectorFactory.AppComponent_Factory [as factory] (app.component.ts:10)
at getNodeInjectable (core.mjs:3556)
at instantiateRootComponent (core.mjs:10159)
at createRootComponent (core.mjs:12259)
at ComponentFactory.create (core.mjs:21580)
My Service:
// imports ...
/**
* DoctorService
*/
#Injectable({
providedIn: "root"
})
export class DoctorService {
constructor(private readonly httpClient: HttpClient) { }
/**
* Returns a doctor by a given uuid.
*
* #param uuid
* #return Promise<Doctor>
*/
public async getDoctorByUUID(uuid: string): Promise<Doctor> {
const uuidRegExp = new RegExp(/[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}/);
if (!uuidRegExp.test(uuid)) {
console.error("UUID does not have a valid format. It needs to be in a 8-4-4-4-12 digit pattern.")
return new Promise<Doctor>(() => {
return [];
});
}
const httpParams: HttpParams = new HttpParams();
httpParams.set("module", "mydoc");
httpParams.set("sektion", "show_doctor");
httpParams.set("uuid", uuid);
httpParams.set("return", "json");
const request = this.httpClient.get<Doctor>(API_BASE_URL, { params: httpParams });
return firstValueFrom(request);
}
}
Component which uses the Service:
import {Component} from '#angular/core';
import {DoctorService} from "my-doc-util/src/lib/services/doctor-service/doctor.service";
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'angular-testing';
constructor(private readonly doctorService: DoctorService) {
}
}
I have experienced the same exact error (angular 11)
I don't know if it's the same reason but in my case, it turned out that the component had an uninitialized input.
I am using ngrx/data and what I need to do is set a value in the store, let's call this ID. And then when I make any request to an entity to pull that ID from the store. I will use update as an example.
Here is an example of a Client Entity Service. I can easily map the returned data as super.update returns an observable.
import { Injectable } from '#angular/core';
import { EntityCollectionServiceBase, EntityCollectionServiceElementsFactory } from '#ngrx/data';
import { Client } from '../../store/client/client.model';
import { Observable } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class ClientEntityService extends EntityCollectionServiceBase<Client> {
constructor(
serviceElementsFactory: EntityCollectionServiceElementsFactory,
) {
super('Client', serviceElementsFactory);
}
public update(entity: Partial<Client>): Observable<Client> {
return super.update(entity);
}
}
However I want to use a store value to make the update. So focusing on the update I can do this:
public update(entity: Partial<Client>): Observable<Client> {
this.store.pipe(
tap((store) => {
console.log(store);
})
).subscribe();
return super.update(entity);
}
Which prints out the store and I can see the value I need, so I could do this
public update(update: Partial<Client>): Observable<Client> {
return this.store.pipe(
select(getClientId),
take(1)
).subscribe((id) => {
return super.update({
id,
...update
});
});
}
However it is requiring me to subscribe to the observable to be able to trigger it. That would mean the caller could not pipe the results and is generally not ideal.
I am wondering if anyone knows of a good solution to be able to get the data from the store but not have to subscribe like I am doing above to get the data, ideally I would want to use a switchMap like this:
public update(update: Partial<Client>): Observable<Client> {
return this.store.pipe(
select(getClientId),
switchMap((id) => {
return super.update({
id,
...update
});
}),
take(1)
)
Thanks
You wrote it correctly in your ideal solution. The difference is that you just need to move take(1) to be right after the select.
public update(update: Partial<Client>): Observable<Client> {
return this.store.pipe(
select(getClientId),
take(1),
switchMap((id) => {
return super.update({
id,
...update
});
}),
)
so store won't cause update requests on every change.
After stepping into the super.update call I could see that the dispatcher was calling:
update(entity, options) {
// update entity might be a partial of T but must at least have its key.
// pass the Update<T> structure as the payload
/** #type {?} */
const update = this.toUpdate(entity);
options = this.setSaveEntityActionOptions(options, this.defaultDispatcherOptions.optimisticUpdate);
/** #type {?} */
const action = this.createEntityAction(EntityOp.SAVE_UPDATE_ONE, update, options);
if (options.isOptimistic) {
this.guard.mustBeUpdate(action);
}
this.dispatch(action);
return this.getResponseData$(options.correlationId).pipe(
// Use the update entity data id to get the entity from the collection
// as might be different from the entity returned from the server
// because the id changed or there are unsaved changes.
map((/**
* #param {?} updateData
* #return {?}
*/
updateData => updateData.changes)), withLatestFrom(this.entityCollection$), map((/**
* #param {?} __0
* #return {?}
*/
([e, collection]) => (/** #type {?} */ (collection.entities[this.selectId((/** #type {?} */ (e)))])))), shareReplay(1));
}
Which effectively just dispatches some actions and then creates a selected observable from this.getResponseData$ using the correlationid etc.
In my use case because I am using the store to get the Id of the current client I don't need the updated client returned as I already have an observable.
On top of the ClientEntityService I have another facade which I am calling ClientService.
Which looks like this:
#Injectable({
providedIn: 'root'
})
export class ClientService {
constructor(
private clientEntityService: ClientEntityService,
private store: Store<AppState>
) {}
public getCurrentClient(): Observable<Client> {
return this.clientEntityService.entityMap$.pipe(
withLatestFrom(this.store.pipe(select(getCurrentId))),
map(([clients, currentId]) => clients[currentId])
);
}
public updateCurrentClient(update: Partial<Client>): Subscription {
return this.getCurrentClient().pipe(
take(1),
switchMap((client) => {
return this.clientEntityService.update({
id: client.id,
...update
});
})
).subscribe();
}
}
So now from within my component I have the constructor
constructor(
private clientService: ClientService,
) {
this.client$ = this.clientService.getCurrentClient();
}
And then on update I call:
this.clientService.updateCurrentClient(theUpdate);
And because I already have this.client$ as an observable of that client being updated I don't need updateCurrentClient to return Observable<Client>. So as per above I am just returning Subscription
I could modify updateCurrentClient to implement something similar to what the DefaultDataService returns, but I'd expect that could be subject to change in future versions. So for now. I am happy with this solution.
I have an component where i am adding a new object called customer by calling the api like this:
public onAdd(): void {
this.myCustomer = this.customerForm.value;
this.myService.addCustomer(this.myCustome).subscribe(
() => { // If POST is success
this.callSuccessMethod();
},
(error) => { // If POST is failed
this.callFailureMethod();
},
);
}
Service file:
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { Observable, Subject } from 'rxjs';
import {ICustomer } from 'src/app/models/app.models';
#Injectable({
providedIn: 'root',
})
export class MyService {
private baseUrl : string = '....URL....';
constructor(private http: HttpClient) {}
public addCustomer(customer: ICustomer): Observable<object> {
const apiUrl: string = `${this.baseUrl}/customers`;
return this.http.post(apiUrl, customer);
}
}
As shown in component code, i have already subscribed the api call like this:
this.myService.addCustomer(this.myCustome).subscribe(
() => { // If POST is success
.....
},
(error) => { // If POST is failed
...
},
);
But,I want to subscribe the results in another component, I have tried like this:
public getAddedCustomer() {
this.myService.addCustomer().subscribe(
(data:ICustomer) => {
this.addedCustomer.id = data.id; <======
}
);
}
I am getting this lint error: Expected 1 arguments, but got 0 since i am not passing any parameter.
What is the right approach to subscribe the api call in other components? after POST operation.
Because i want to get added object id for other functionality.
Well it totally depends on the design of your application and the relation between components. You can use Subjects for multicasting the data to multiple subscribers.
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { Observable, Subject } from 'rxjs';
import { ICustomer } from 'src/app/models/app.models';
#Injectable({
providedIn: 'root',
})
export class MyService {
private baseUrl : string = '....URL....';
private latestAddedCustomer = new Subject();
public latestAddedCustomer$ = this.latestAddedCustomer.asObservable()
constructor(private http: HttpClient) {}
public addCustomer(customer: ICustomer): Observable<object> {
const apiUrl: string = `${this.baseUrl}/customers`;
return this.http.post(apiUrl, customer).pipe(map((data) => this.latestAddedCustomer.next(data)));
}
}
and subscribing to the subject as follows
this.latestAddedCustomer$.subscribe()
should get you the latest added customer details. Even though i would not do this the way its written. I would basically write a seperate service to share the data between the components or would write a cache service if its used across the application. But the idea here is to use the concept of Subjects. You can read more about it Here
I'm new to Angular and TypeScript and just started working on a project using MEAN stack (MongoDB, Express, Angular, Node.js).
I created this mongoose module :
import * as mongoose from 'mongoose';
var Schema = mongoose.Schema;
const entrepriseSchema = new mongoose.Schema({
name: {type: String, unique: true, required : true},
telephone: Number,
logo: String,
web_site: String,
sites: [
{site_id: {type: Schema.Types.ObjectId, ref: 'Site'}}
]
});
const Entreprise = mongoose.model('Entreprise', entrepriseSchema);
export default Entreprise;
and this is my entreprise.component.ts :
import { Component, OnInit } from '#angular/core';
import { Http } from '#angular/http';
import { FormGroup, FormControl, Validators, FormBuilder } from '#angular/forms';
import { ActivatedRoute } from '#angular/router';
import { EntrepriseService } from '../services/entreprise.service';
import { SiteService } from '../services/site.service';
#Component({
selector: 'app-entreprise',
templateUrl: './entreprise.component.html',
styleUrls: ['./entreprise.component.scss'],
providers: [EntrepriseService, SiteService]
})
export class EntrepriseComponent implements OnInit {
entreprise = {};
sites = [];
id: String;
constructor(private entrepriseService: EntrepriseService,
private siteService: SiteService,
private http: Http,
private route: ActivatedRoute) {
this.id = route.snapshot.params['id'];
}
ngOnInit() {
this.getEntrepriseById(this.id);
//not working
//console.log(this.entreprise.name);
//console.log(this.entreprise.sites);
//this.getSitesIn(this.entreprise.sites);
}
getEntrepriseById(id) {
this.entrepriseService.getEntreprise(id).subscribe(
data => this.entreprise = data,
error => console.log(error)
);
}
getSitesIn(ids) {
this.siteService.getSitesIn(ids).subscribe(
data => this.sites = data,
error => console.log(error)
);
}
}
when I try to display the properties of the returned from entreprise.component.html it works fine and displays all the properties :
<h3>{{entreprise.name}}</h3>
<div *ngFor="let site of entreprise.sites">
{{site.site_id}}
</div>
{{entreprise.logo}}
{{entreprise.web_site}}
but how can I access the same properties on the TypeScript side ?
The commented code in the EntrepriseComponent is what I'm trying to accomplish but it's not working since this.entreprise is type {} .
The Enterprise model/schema that you created in Mongoose in Node.js resides on the server side. If you want the TypeScript code on the UI to recognize the properties in Enterprise, you will have to create a class in your angular codebase.
Create a folder named, say, models at the same level as your services folder. (Optional)
Create two files named site.ts and enterprise.ts in the models folder created in the previous step (You can put these file at a different location if you want) with the following contents:
site.ts
export interface Site {
site_id?: string;
}
enterprise.ts
import { Site } from './site';
export interface Enterprise {
name?: string;
telephone?: string;
logo?: string;
web_site?: string;
sites?: Site[];
}
Now, inside the EntrepriseComponent file, add the following imports
import { Enterprise} from '../models/entreprise';
import { Site } from '../models/site';
And change the first lines inside the EntrepriseComponent file to
export class EntrepriseComponent implements OnInit {
entreprise: Enterprise = {};
sites: Site[] = [];
Now, the enterprise attribute will be of type Enterprise and you will be able to access the properties that we declared in the enterprise.ts file.
Update:
Also, you cannot console.log(this.enterprise.name) immediately after this.getEntrepriseById(this.id); in your ngOnInit() function. This is because the web service you are making to get the enterprise object would not have resolved when you are trying to log it to the console.
If you want to see the enterprise object in the console or you want to run some code that needs to run after the service call has resolved and the this.enterprise object has a value, the best place to do this would be your getEntrepriseById function. Change the getEntrepriseById function to
getEntrepriseById(id) {
this.entrepriseService.getEntreprise(id).subscribe(
data => {
this.enterprise = data;
console.log(this.enterprise.name);
// Any code to run after this.enterprise resolves can go here.
},
error => console.log(error)
);
}
I have used Promise and observables logic to fetch data from server using "get".
It was working till yesterday. SUddenly it starts throwing the above error.
Please help me finding the error.
I am getting "Generic type 'Promise' requires 1 type argument(s)" error.
#Injectable()
export class myBlogService{
// Property to hold root server URL i.e host
private serverUrl:string = "app/data.json"
constructor(private http:Http) {}
// check function in service to check control is coming to service
check(){
alert("getting clicked from service");
}
// get function to get data from server
// basically blog datas
get(): Promise {
return this.http.get(this.serverUrl)
.map(response => response.json())
}
}
/**
*
* My Components
*
*/
#Component({
selector: 'my-app',
providers: [myBlogService],
styleUrls: ['app/css/app.css'],
template: `
<h1 (click)= check()>My First Angular 2 App</h1>
<button (click)=get()>Get My Name</button>
<h1>{{getResponse.name}}</h1>
`
})
export class myBlogApp {
// Property to hold blog data
public getResponse = {"name": "", "age": ""};
constructor(protected myblogservice:myBlogService){}
// check function to check control is going to service
check() {
this.myblogservice.check();
}
// get function calls service get function which return data from server
get(){
this.myblogservice.get().subscribe(data => {
this.getResponse = data;
});
}
}
/**
*
* NgModule Declaration
*
*/
#NgModule({
imports: [ BrowserModule, HttpModule ],
declarations: [ myBlogApp ],
providers: [ ],
bootstrap: [ myBlogApp ]
})
export class app{}
/**
*
* App engine entry point
*
*/
const platform = platformBrowserDynamic();
platform.bootstrapModule(app);
When "promise: " is given, still it gives issue like
"error TS2339: Property 'subscribe' does not exist on type 'Promise'".
I tried different solution but no luck yet.
You need to add the specific type.
If it contains no data and is being used purely for the resolve/reject functionality, use:
Promise<void>
Ultimately this is a type signature like any other, so you can use:
Promise<any>
https://basarat.gitbooks.io/typescript/content/docs/promise.html
Instead of using Promise try to use Observable, replace:
get(): Promise {
return this.http.get(this.serverUrl)
.map(response => response.json())
}
with
get(): Observable<any> {
return this.http.get(this.serverUrl)
.map(response => response.json())
}