Deserialize and cyclic dependencies in Angular - json

I have several model.ts files.
When I use httpClient, I get a JSON object, but it does not work correctly since I have to deserialize them: How to recursively init class get by httpclient.
BUT since so, I found the project "class-transformer" that help me deserialize all my models.
I have in my services:
public method(cli: any): Observable<A> {
const formData = new FormData();
formData.append('cli', JSON.stringify(cli));
return this.http.post<A>('/my/url',
formData, {
withCredentials: true
}).pipe(first(),
map(res => {
return plainToClass(A, res);
})
);
}
And for models something like:
// A.model.ts
import { Type } from 'class-transformer';
import { B } from './B.model';
export class A {
// Some properties
#Type(() => B)
b: B[]
// Some methods
}
And B
// B.model.ts
import { Type } from 'class-transformer';
import { A } from './A.model';
export class B {
// Some properties
#Type(() => A)
a: A[]
// Some methods
}
But, when compiling I got "Circular dependency" and indeed there is a circular dependency...
Looking for solution I understant that I can use a Barrel (https://github.com/typestack/class-transformer/issues/230) but it did not work.
My only constraint is that I have to keep this relation -> (or something really similar since I can not modify the backend and so data I will receive with httpClient).
Any idea on how to fix the cyclic dependency?

Finally, I used the barrel solution. It appears that in the rest of my code there were import of class A directly, I changed them to use the barrel and everything works (I still have warnings. But it works)
// C.model.ts
// the problem is here
import { A } from './bla/A.model';
export class C {
}
// C.model.ts
import { A } from './bla';
export class C {
}
And at ./bla I have a index.ts with all the model export

Related

How can I use the data obtained from a local json file in Angular?

I am using Angular and have stored a json file with data in it locally.
I am accessing the json file by importing it into my component through:
import * as data from '../../data/countries.json';
In my tsconfig.json file, I have set the following:
"resolveJsonModule": true
I am running into issues when using the data set.
The following works:
console.log(data[0].Country); and this returns me the name of the first country in the list, printing it to the chrome console.
However, when I attempt to use this data within the component.ts code, I get the following errors:
Code:
for (let i = 0; i < 50; i++) {
let name :string = data[i].Country;
this.addCoordinates(name, data[i].latitude, data[i].longitude);
}
Error:
core.js:6210 ERROR TypeError: Cannot read property 'Country' of undefined
at GlobeComponent.changeCountry (globe.component.ts:208)
at GlobeComponent.ngAfterViewInit (globe.component.ts:75)
at callHook (core.js:2573)
at callHooks (core.js:2542)
at executeInitAndCheckHooks (core.js:2493)
at refreshView (core.js:9537)
at refreshComponent (core.js:10637)
at refreshChildComponents (core.js:9263)
at refreshView (core.js:9516)
at renderComponentOrTemplate (core.js:9580)
Any help would be much appreciated! Thanks in advance.
I found a solution:
I've created a service that reads the JSON file through the HttpClient and returns the array of objects stores in your file.
This is the service:
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class LocalJSONService {
constructor(private http: HttpClient) { }
getLocalJSON$(): Observable<any>{
return this.http.get(`../assets/countries.json`);
}
}
And this is the globe.component.ts:
First inject the new service on the controller:
constructor(private localJSON: LocalJSONService) {
...
}
And then on the ngOnInit (you may place it on the ngAfterViewInit probably) I call a function called getLocalJSON$:
getLocalJSON$(): void {
this.localJSON
.getLocalJSON$()
.pipe(first())
.subscribe((countries: any[]) => {
countries.map((country: any) => console.log(country));
})
}
Instead of iterate over the countries you can store the values or call another function, whatever you need.

Angular observable subscribe JSON parsing issue

I have a service performing http.get on a Drupal API and retrieving JSON data.
The component utilising that JSON data keeps generating the following error:
ERROR in src/app/form-test/form-test.component.ts(18,28): error TS2551: Property 'included' does not exist on type 'Question[]'. Did you mean 'includes'?
From the following code:
constructor(private dataService: QuizService) { }
ngOnInit() {
this.dataService.fetch().subscribe(data => {
this.jsondata = data.included[0].attributes.field_json;
console.log(data, ': DATA');
});
}
I don't understand why there is a problem with the JSON and why it's trying to find includes instead of included in the JSON structure. Below is a screenshot of a sample of the JSON:
I have confirmed the structure of the JSON data (as confirmed from the image above), also from console logging the JSON data and that the API URL is live at the time Ay angular app is attempting to call it.
Can anyone advice what is the cause of this error and how can I resolve it?
UPDATE:
quiz.service.ts:
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable } from 'rxjs';
export interface Question {
// title: string;
question: string;
included: any[];
}
#Injectable({
providedIn: 'root'
})
export class QuizService {
// tslint:disable-next-line: max-line-length
private quizURL: string = 'http://drupal-8-composer-drupal-test.com/jsonapi/node/quiz/31f020f7-34d9-4b9a-bd2b-0d567eb285dc/?include=field_questions&fields%5Bnode--quiz%5D=title,drupal_internal__nid,body&fields%5Bnode--question%5D=title,field_processed,body,field_options,field_json';
constructor(private httpClient: HttpClient) { }
fetch(): Observable<Question[]> {
return this.httpClient.get<Question[]>( this.quizURL );
}
}
The error states that data has type Question[]. It is an array, not an object. Typescript compiler tries to find an included variable in array and there's none. So it gives you an error.
Your JSON structure contains an array of questions in the included field. So the type which the fetch returns should be like { included: Question[] }:
fetch(): Observable<{ included: Question[] }> {
return this.httpClient.get<{ included: Question[] }>( this.quizURL );
}
Or you can process the response in service and return questions only:
fetch(): Observable<Question[]> {
return this.httpClient.get(this.quizURL)
.pipe(map((data: { included: Question[] }) => data.included));
}
.map operator gets the whole response object, extracts only questions and returns them as array.

How can I parse JSON attributes with slashes properly?

I have 2 Interfaces:
DTO.ts
export interface Report{
berichtId: number,
summary: Label
}
export interface Label{
text: string
}
I use them to type my HttpClient get request, to receive an array of Reports:
Dao.service.ts
getReports():Observable<DTO.Report[]>{
return this.http.get<DTO.Report[]>(Dao.API+'report/current')
.pipe(
retry(3),
catchError(this.handleErrors)
);
What I get is this JSON:
[
{
"berichtId":1777,
"summary":
"{\"text\":\"asdf\"}"
}
]
Now I want to read them out but these slashes dont let me convert the JSON to my pre defined interface Report object. It just translates it to a normal string.
this.dao.getReports().subscribe(report=>{
console.log(report[0].summary); // {"text":"asdf"}
console.log(report[0].summary.text) // undefined
});
What is the best way to handle that problem? There are solutions online but they are often rather counter intuitive. There must be a better way in Angular.
I think you need to use map operator from rxjs/operators before returniing data from service.
I have intercepted the data using map (rxjs) and then parsed the summary.
Demo (check console on button click)
Service
import { Injectable } from '#angular/core';
import { of, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Report } from './types';
import { HttpClient } from '#angular/common/http';
#Injectable()
export class DataService {
constructor(private http: HttpClient) { }
getReports(): Observable<Report[]> {
return this.http.get<any[]>('api')
.pipe(
map(
values =>
values.map(val => {
val.summary = JSON.parse(val.summary)
return val;
}
)
));
}
// mock methos for demo
mockGetReports() {
return of<any[]>([
{
"berichtId":1777,
"summary":
"{\"text\":\"asdf\"}"
}
])
.pipe(
map(
values =>
values.map(val => {
val.summary = JSON.parse(val.summary)
return val;
}
)
));
}
}
I think you should try JSON.parse() or if it does not help then you can try this npm library
I hope this solves your issue.

String Resources in Angular

I'm developing an Angular app and I'm looking for something similar to Android Resource available in Android development.
Here is the way to get a string in Android:
String mystring = getResources().getString(R.string.mystring);
I would like to have the same in Angular.
For example if I have few HTML templates in which there are the same message about the wrong email provided...
<div class="alert alert-danger">
<strong>Error!</strong>Invalid e-mail
</div>
I would like to have the following:
<div class="alert alert-danger">
<strong>Error!</strong>{{myStrings.INVALID_EMAIL}}
</div>
...or something like this...
<div class="alert alert-danger">
<strong>Error!</strong>{{'INVALID_EMAIL' | stringGenerator}}
</div>
Do you know a way or addon I can install to reach that?
Having configuration, translations and resources separated from application's logic is very useful. Configuration would also be very helpful in other context like, for example, getting api_url useful for any rest call.
You can set up such thing using #angular/cli. Having the following application structure:
|- app
|- assets
|- i18n
- en.json
- it.json
|- json-config
- development.json
- env.json
- production.json
|- resources
- en.json
- it.json
|- environment
- environment.prod.ts
- environment.ts
|- config
- app.config.ts
Where:
app: contain all application logics
assets/i18n/*.json: contains a textual resources that can be used in any of your components. There's one of them for each language we want to cover.
E.G. en.json:
{
"TEST": {
"WELCOME" : "Welcome"
}
E.G it.json:
{
"TEST": {
"WELCOME" : "Benvenuto"
}
assets/json-config: contains configuration files to use in development mode and production mode. Also contains env.json which is a json that says which is the current development mode:
E.G. env.json:
{
"env" : "development"
}
E.G. development.json:
{
"API_URL" : "someurl",
"MYTOKEN" : "sometoken",
"DEBUGGING" : true
}
assets/resources: contains jsons files of resources per each language we want to cover. For instance, it may contain jsons initialization's for application models. It's useful if, for example, you want to fill an array of a model to be passed to an *ngFor personalized based on enviroment and/or language. Such initialization should be done inside each component which want to access a precise resource via AppConfig.getResourceByKey that will be shown later.
app.config.ts: Configuration Service that loads resources based on development mode. I will show a snippet below.
Basic Configuration:
In order to load basic configuration files as the application starts we need to do a few things.
app.module.ts:
import { NgModule, APP_INITIALIZER } from '#angular/core';
/** App Services **/
import { AppConfig } from '../config/app.config';
import { TranslationConfigModule } from './shared/modules/translation.config.module';
// Calling load to get configuration + translation
export function initResources(config: AppConfig, translate: TranslationConfigModule) {
return () => config.load(translate);
}
// Initializing Resources and Translation as soon as possible
#NgModule({
. . .
imports: [
. . .
TranslationConfigModule
],
providers: [
AppConfig, {
provide: APP_INITIALIZER,
useFactory: initResources,
deps: [AppConfig, TranslationConfigModule],
multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
app.config.ts:
As said above, this service loads configuration files based on development mode and, in this case, browser language. Loading resources based on language can be very useful if you want to customize your application. For example, your italian distribution would have different routes, different behavior or simple different texts.
Every Resources, Configuration and Enviroment entry is available trough AppConfig service's methods such as getEnvByKey, getEntryByKey and getResourceByKey.
import { Inject, Injectable } from '#angular/core';
import { Http } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { get } from 'lodash';
import 'rxjs/add/operator/catch';
import { TranslationConfigModule } from '../app/shared/modules/translation.config.module';
#Injectable()
export class AppConfig {
private _configurations: any = new Object();
private _config_path = './assets/json-config/';
private _resources_path = './assets/resources/';
constructor( private http: Http) { }
// Get an Environment Entry by Key
public getEnvByKey(key: any): any {
return this._configurations.env[key];
}
// Get a Configuration Entryby Key
public getEntryByKey(key: any): any {
return this._configurations.config[key];
}
// Get a Resource Entry by Key
public getResourceByKey(key: any): any {
return get(this._configurations.resource, key);
}
// Should be self-explanatory
public load(translate: TranslationConfigModule){
return new Promise((resolve, reject) => {
// Given env.json
this.loadFile(this._config_path + 'env.json').then((envData: any) => {
this._configurations.env = envData;
// Load production or development configuration file based on before
this.loadFile(this._config_path + envData.env + '.json').then((conf: any) => {
this._configurations.config = conf;
// Load resources files based on browser language
this.loadFile(this._resources_path + translate.getBrowserLang() +'.json').then((resource: any) => {
this._configurations.resource = resource;
return resolve(true);
});
});
});
});
}
private loadFile(path: string){
return new Promise((resolve, reject) => {
this.http.get(path)
.map(res => res.json())
.catch((error: any) => {
console.error(error);
return Observable.throw(error.json().error || 'Server error');
})
.subscribe((res_data) => {
return resolve(res_data);
})
});
}
}
translation.config.module.ts
This module sets up translation built using ngx-translate. Sets up translation depending on the browser language.
import { HttpModule, Http } from '#angular/http';
import { NgModule, ModuleWithProviders } from '#angular/core';
import { TranslateModule, TranslateLoader, TranslateService } from '#ngx-translate/core';
import { TranslateHttpLoader } from '#ngx-translate/http-loader';
import { isNull, isUndefined } from 'lodash';
export function HttpLoaderFactory(http: Http) {
return new TranslateHttpLoader(http, '../../../assets/i18n/', '.json');
}
const translationOptions = {
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [Http]
}
};
#NgModule({
imports: [TranslateModule.forRoot(translationOptions)],
exports: [TranslateModule],
providers: [TranslateService]
})
export class TranslationConfigModule {
private browserLang;
/**
* #param translate {TranslateService}
*/
constructor(private translate: TranslateService) {
// Setting up Translations
translate.addLangs(['en', 'it']);
translate.setDefaultLang('en');
this.browserLang = translate.getBrowserLang();
translate.use(this.browserLang.match(/en|it/) ? this.browserLang : 'en');
}
public getBrowserLang(){
if(isUndefined(this.browserLang) || isNull(this.browserLang)){
this.browserLang = 'en';
}
return this.browserLang;
}
}
Ok, and now? How can I use such configuration?
Any Module/Component imported into app.module.ts or any of them that is imported into another custom module that is importing translation.config.module can now automatically translate any interpolated entry based on browser language. For instance using the following snipped will generate Welcome or Benvenuto based on explained behavior:
{{ 'TEST.WELCOME' | translate }}
What If I want to get a resource to initialize a certain array that will be passed to an *ngFor?
In any component, just do that inside the constructor:
. . .
// Just some model
public navigationLinks: NavigationLinkModel[];
constructor(private _config: AppConfig) {
// PAGES.HOMEPAGE.SIDENAV.NAVIGATION contains such model data
this.navigationLinks =
this._config.getResourceByKey('PAGES.HOMEPAGE.SIDENAV.NAVIGATION');
}
Of course you can also combinate resources and configuration.
AndreaM16's answer is definitely thorough, but you sould also consider using an existing package for this instead of writing your own custom code that you need to maintain. To that end, I would recommend checking out ngx-translate (which is used somewhat under the hood in that answer), transloco and Angular's built-in i18n capabilities.
Most of those approaches are based on the magic strings, where your HTML templates and typescript code need to contain strings that (hopefully) match the keys in a JSON or XML file. This presents a serious code maintenance issue which won't be revealed at compile time, only at runtime. I would also advocate for checking out some guides on creating a type-safe translation system:
https://medium.com/angular-in-depth/angular-typed-translations-29353f0a60bc
https://medium.com/angular-in-depth/dynamic-import-of-locales-in-angular-b994d3c07197
You can also use enum file for string values.
string.ts
export enum TEST_STRING {
TEST_ONE = "this is first test string",
TEST_TWO = "this is second test string",
TEST_THREE = "this is third test string"
}
component.ts
import { TEST_STRING } from '../resources/string';
var testString = TEST_STRING.TEST_ONE;
console.log(testString); // result = this is first test string

Missing props in mapStateToProps

Version
4.1.1
Steps to reproduce
This component is querying a db via GraphQL thanks to Apollo. It is a GraphQL container.
It uses compose from react-apollo library in order to use multiple enhanchers at the same time, in a readable way.
Example component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { graphql, compose, withApollo } from 'react-apollo';
import { myQuery } from './graphqlQuery';
class MyComponent extends Component {
render() {
const { exampleProp } = this.props;
return null;
}
}
function mapStateToProps(state, ownProps) {
// I expect to see ownProps.exampleProp here but it is undefined
console.log(ownProps.exampleProp);
}
export default compose(
withApollo,
connect(mapStateToProps),
graphql(myQuery),
)(MyComponent);
Expected Behavior
ownProps should contain the props passed to the component as stated here
like:
Object = {
client: ApolloClient
exampleProp: "propcontent", // <-- this is going to be lost
history: Object
location: Object
match: Object
staticContext: undefined
__proto__: Object
}
Actual Behavior
instead ownProps contains only this:
Object = {
client: ApolloClient
history: Object
location: Object
match: Object
staticContext: undefined
__proto__: Object
}
All the props that the component should have had from parents and from GraphQL response are missing, including, in this case, exampleProp.