Converting httpClient answer to model objects [Angular 6] - javascript

I have a question about the Angular 5 httpClient.
This is a model class with a method foo() I'd like to receive from the server
export class MyClass implements Deserializable{
id: number;
title: string;
deserialize(input: any) {
Object.assign(this, input);
return this;
}
foo(): string {
// return "some string conversion" with this.title
}
}
This is my service requesting it:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable } from 'rxjs';
import { MyClass } from './MyClass';
#Injectable({
providedIn: 'root',
})
export class MyClassService {
constructor(private http: HttpClient) {
}
getMyStuff(): Observable<MyClass[]> {
// this is where I hope to convert the json to instances of MyClass
return this.http.get<MyClass[]>('api/stuff')
}
}
My Problem
When I ask the service for instances of MyClass I get the data, but I cannot run {{ item.foo() }} in the template. Also, when I console.log() the typeof of an item where it is received in the service, I do no see instances of an object of MyClass.
What am I doing wrong? I thought that writing this.http.get<MyClass[]>('api/stuff') would do the conversion.
Any hints? Thank you in advance!

When doing that, TypeScript only does "type assertion". It means that you're telling TypeScript that your object is of type MyClass, but the object isn't actually an instance of MyClass at runtime. In order to call functions defined in your model object, you have to define constructors in your model classes like that :
constructor(obj?: any) {
Object.assign(this, obj);
}
Then in your services add a mapping like this :
http.get<MyClass>('/my-class').pipe(
map(res => new MyClass(res))
Note: the code above is RxJS 6 style, i don't know which version you are using

It works for me like this
import { HttpClient } from '#angular/common/http';
...
this.httpClient.get<MyResponse>('http://......').toPromise()
.then((myResponse) => {
console.log('myResponse.myField: ' + JSON.stringify(tokenResponse));
})
.catch((error) => {
console.log('Promise rejected with ' + JSON.stringify(error));
});
...
interface MyResponse {
myField: string;
myOtherField: string;
}

Related

how to subscribe the Id of the created object after POST operation

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

Assign class instance property a value based on its type in Typescript Angular

I don't know if this is allowed in Typescript, but I'm working in an Angular 7 project and I want to instantiate a Page class fullfilling all his properties from DB object. These are my classes:
export class User {
id: number;
name: string;
created_at: string;
constructor(obj?: any) {
Object.assign(this, obj);
}
getName(): string {
return this.name;
}
}
export class Page {
id: number;
title: string;
author: User;
constructor(obj?: any) {
Object.assign(this, obj);
}
showTitle(): string {
return this.title;
}
}
Here is an example of my service method to retrieve the data:
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Page } from '../models/page';
#Injectable()
export class PageService {
constructor(httpClient: HttpClient) {}
getPage(id: number): Observable<Page> {
return this.httpClient
.get<Page>('http://<my-server-ip>:<port>/api/pages')
.pipe(
map((page: Page) => {
console.log('retrieved', page);
return new Page(page);
})
);
}
}
And here is an example of this function call in my component
export class MyCustomComponent implements OnInit {
constructor(pageService: PageService) {}
ngOnInit() {
this.pageService.getPage()
.subscribe((page: Page) => {
console.log(page.showTitle());
});
}
}
This example works, but when I want to access to User methods, like:
console.log(page.author.getName());
I don't have access to them because it is not an instantiation of User class.
The same would happen with Page if I do not return a new instance of page class as an observable, thats why I use return new Page(page) after retrieving the data.
The problem is that I want to keep my constructors as generic as possible, so creating a constructor to assign the value manually (e.g.: this.author = new User(obj.author);) is not a valid workaround, as I want to implement it in every model or create a GenericModel then extend all my models.
Is there a way to fill a property with defined type in a instantiated class depending in its type?
This is what I tried so far, but it doesn't work:
export class Page {
// ...
constructor(obj?: any) {
Object.keys(obj).forEach((key: string, index: number) => {
if (typeof(obj[key]) === 'object' && obj[key] !== null) {
this[key] = new (this[key].constructor)(obj[key]);
} else {
this[key] = obj[key]
}
});
}
}
ERROR TypeError: Cannot read property 'author' of null
ERROR TypeError: Cannot read property 'constructor' of undefined
I understand that this is null when constructor is called, but I couldn't find another way to fill author property with a new instance to access to methods. Also, if I get a standard/default object like { ... }, the if will trigger and probably will throw an error too, as it does not have a constructor.
You could use Object.assign like this:
getPage(id: number): Observable<Page> {
return this.httpClient
.get<Page>('http://<my-server-ip>:<port>/api/pages')
.pipe(
map((page: Page) => {
console.log('retrieved', page);
return Object.assign(new Page(), page);
})
);
}
This code creates a new Page instance and then copies over all of the properties from the returned response (page in this example).
Then you don't need to modify your constructors.
UPDATE
NOTE: The spread syntax only copies over the properties, so I changed to use Object.assign instead.

how to access object properties from TypeScript?

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)
);
}

Class lacks methods after passed in Observable chain

I encounter a strange problem with my custom class in angular2 after passing it through an Observable chain.
I always receive the error:
EXCEPTION: f.mapToParams is not a function
ORIGINAL STACKTRACE:
TypeError: f.mapToParams is not a function
at SafeSubscriber._next (filter.component.ts)
...
Uncaught TypeError: f.mapToParams is not a function
at Safesubscriber._next (filter.component.ts)
Here is my coding:
filter.ts:
import { Params } from '#angular/router';
export class Filter {
public text:String = '';
public mapToParams():Params {
let params:Params = {};
// Do some mapping here...
return params;
}
}
filter.component.ts
import { Component, OnInit Output, EventEmitter } from '#angular/core';
import { Router, Params } from '#angular/router';
import { Filter } from './filter';
import { Observable, Subject } from 'rxjs/Rx';
export class FilterComponent implements OnInit {
private _filter:Filter;
private _filterStream = new Subject<Filter>();
ngOnInit() {
this._filter = new Filter();
this._filterStream
.debounceTime(300)
.switchMap((f:Filter) => Observable.of(f))
.subscribe((f:Filter) => {
let params:Params = {};
console.log(f.text); // <-- No problem here
// params = this._map(f); // <-- This would work
params = f.mapToParams(); // <-- Here occurs the error
});
}
private _map(f:Filter):Params {
// Do some mapping here
}
public onInputChanged(searchText:String):void {
this._mergeFilter( {
map(f:Filter) {
f.text = searchText;
}
})
}
private _mergeFilter(callback:FilterMergeCallback):void {
let f:Filter = JSON.parse(JSON.stringify(this._filter));
callback.map(f);
this._filterStream.next(f);
}
}
I have tried to comment out the debounceTime and switchMap statement but with no success.
At a different point in my coding the filter.mapToParams method can be called without any problems. It seems to me like the Observable chain strips all methods from my object.
Here is my angular config:
#angular/cli: 1.0.0.-beta.32.3
#angular/common: ^2.4.0
#angular/compiler ^2.4.0
#angular/core ^2.4.0
rxjs: ^5.1.0
Can anyone help me on this?
I think I got it:
I copied the current _filter Object to a new filter variable by
JSON.parse(JSON.stringify())
By this, all methods get stripped from the new object.
Means, I have to find a new method to clone the object...
Thanks all for you replies!

Angular2 Service Returns Undefined

For some reason my services aren't working. I've been lurking SO for two days trying to find similar questions, but they don't fit my problem.
Service.ts:
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import { CarObject } from './make';
#Injectable()
export class EdmundsService {
private stylesurl = 'REDACTED';
constructor(private http: Http) { }
getCars(): Observable<CarObject[]> {
return this.http.get(this.stylesurl)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
private handleError (error: Response | any) {
// In a real world app, we 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);
}
}
These are my 'models':
class Style {
id: number;
name: string;
make: Make;
model: Model;
year: Year;
submodel: Submodel;
trim: string;
states: string[];
engine: Engine;
transmission: Transmission;
options: Options[];
colors: Color[];
drivenWheels: string;
numOfDoors: string;
squishVins: string[];
categories: Categories;
MPG: MPG;
manufacturerOptionCode: string;
}
export class CarObject {
styles: Style[];
stylesCount: number;
}
My component:
import { CarObject } from './make';
import { EdmundsService } from './edmunds-search-result.service';
#Component({REDACTED
providers: [EdmundsService] })
export class EdmundsSearchResultComponent implements OnInit {
cars: CarObject[];
errorMessage: string;
constructor(private _edmundsService: EdmundsService) { }
getCars(): void {
this._edmundsService.getCars()
.subscribe(
cars => this.cars = cars,
error => this.errorMessage = <any>error);
}
ngOnInit(): void {
this.getCars();
}
}
Component HTML:
{{ cars.stylesCount | async }}
Sample API Response: http://pastebin.com/0LyZuPGW
Error Output:
EXCEPTION: Error in ./EdmundsSearchResultComponent class
EdmundsSearchResultComponent - inline template:0:0 caused by:
Cannot read property 'stylesCount' of undefined
CarObject was designed to match the API Response, so it could be okay to remove the array brackets ( [] )
I don't know why this won't display the object data on my template despite closely following the Tour Of Heroes HTTP/Services tutorial.
What I am trying to do is make an HTTP request from variable 'styleurl' (which I see is successfully made by checking the 'Network' tab in chrome dev tools.) Using this API Response, I want my CarObject to 'consume' the json response, and be available to my component/template.
In your component you're reserving your car property, but you don't initialize it, so it remains undefined.
At the time your HTML renders the promise isn't fulfilled yet, your car is still undefined but you try to access a property from it.
A couple solutions:
preset it:
cars: CarObject = new CarObject(); // or <CarObject>{}
use the elvis operator in your template:
{{ cars?.stylesCount }}
use ngIf:
<div *ngIf="cars">{{ cars.styleCount }}</div>
There are probably a couple of more ways to handle this case.
See my update at the bottom regarding your usage of the async pipe. It probably leads to errors as well in the way you're trying to use it.
Besides, i would suggest reading up on TypeScript types as well as general best practices for angular and typescript especially regarding the usage of models, interfaces and such. Also using Observables would be a good idea instead of Promises.
There are some issues in your code, so this is just a hint, but elaborating on them has no place here i think and aren't the source of your problem.
Hope i could help.
Update:
About your usage of the async pipe:
The async pipe subscribes to an Observable or Promise and returns the latest value it has emitted.
You use it on an array of CarObjects which btw shouldn't be an array.
Take a look at the documentation for the async pipe for proper usage.

Categories