Angular Typescript and HTML - html

I have an interface that looks like this:
export interface GeneralInfo{
Name: string;
Description: string;
}
Later in a component class, I have the following code
export class SignalsComponent implements OnInit {
objGeneral: GeneralInfo;
constructor(private _apiService: APIService)
openPopUp(){
this._apiService.getJsonData().subscribe(
(res => {
var tempJson = JSON.parse(res);
this.objGeneral = tempJson.General as GeneralInfo;
console.log("json --->", this.objGeneral.Description);
}),
(err => { })
);
}
}
When I look at the browser console all works and I see the data I expect to see. However, when I try to invoke the objGeneral.Description property in HTML, it fails. This is my HTML:
<div class="col-lg-6 col-md-6">
{{objGeneral.Description}}
</div>
What am I doing wrong?
Thank you

1) Quick answer
Add a condition to your div :
<div class="col-lg-6 col-md-6" *ngIf="objGeneral">
{{objGeneral.Description}}
</div>
Or use optional chaining if you still want to render an empty div :
<div class="col-lg-6 col-md-6">
{{objGeneral?.Description}}
</div>
Note that you can use optional chaining in condition :
<div class="col-lg-6 col-md-6" *ngIf="objGeneral?.Description">
2) Complete answer
objGeneral is still not defined when your DOM is rendering, it will be defined when the Observable completes. Means you are asking the DOM to render undefined data.
When I look at the browser console all works
Weird, because you should have this type of error in your console which prevents you to call the property of an undefined object :
ERROR TypeError: Cannot read properties of undefined (reading 'Description')
3) Ressources & useful links you might want to check for more information
https://javascript.plainenglish.io/the-beauty-of-optional-chaining-in-typescript-32dd58ce1380

The way you do it is incorrect - objGeneral exists only after async API call. Instead you should deliver this property via async pipe
export class SignalsComponent implements OnInit {
objGeneral$: BehavioutSubject<GeneralInfo> | null;
constructor(private _apiService: APIService)
openPopUp(){
this.objGeneral$ = this._apiService.getJsonData().pipe(
map(res => JSON.parse(res).General as GeneralInfo)
);
}
}
and use it as
<div class="col-lg-6 col-md-6" *ngIf="objGeneral$ | async as objGeneral">
{{objGeneral.Description}}
</div>

Related

How to show a loading indicator until all async http calls are completed - Angular

My question is how to show a loading spinner until all of my async http requests are completed. This way I wouldn't show bits and pieces of the screen until all of the data is received from the server.
My biggest issue is that I have components that are triggered specifically through the html, so I can't simply put an *ngIf statement over part of the html when I want to show it.
Here's what I have so far. FYI, the Template variable that currently triggers the visibility of the html is set when one of the http requests complete in this component. I want to wait for the child component's http requests to complete before showing the html, but I must execute the logic in the html in order to call the child components.
The *ngIf statement does NOT currently work in the way I desire, I'm just showing what I'm currently doing.
<div class="col-sm-12"
*ngIf="Template">
<div id="nav" style="height: 200px">
<div id="outer"
style="width: 100%">
<div id="inner">
<o-grid>
</o-grid>
</div>
</div>
</div>
<collapsible-panel *ngFor="let s of Template?.s; let i = index"
[title]="s.header">
<div *ngFor="let c of s.c">
<fact [eC]="c.c"
[label]="c.l">
</fact>
</div>
</collapsible-panel>
<collapsible-panel title="T">
<div>
<i-f >
</i-f>
</div>
</collapsible-panel>
</div>
<div *ngIf="!Template" class="spinner"></div>
EDIT (SOLUTION): Here's the solution I implemented, per the answer below from #danday74.
I instantiated the variable inside of my service where I make all of my http requests. I defined it as true to start, and set it to false in one of the child components when the subscribe completes.
I'll just need to make sure in the future to set cService.asyncRequestsInProgress to false wherever the last async http request takes place, if it ever changes.
Parent HTML:
<div class="col-sm-12"
[ngClass]="{hideMe:cService.asyncRequestsInProgress}">
......
</div>
<div *ngIf="cService.asyncRequestsInProgress" class="spinner"></div>
Service:
#Injectable()
export class CService {
asyncRequestsInProgress: boolean = true;
constructor(public http: HttpClient) { }
}
Child Component (Where the last async request completes):
export class FComponent implements OnInit {
....
doSomething() {
this.cService.getWhatever().subscribe(x => {
this.cService.asyncRequestsInProgress = false;
}
}
}
styles.css
.hideMe {
visibility: hidden;
}
You could use a resolver. A resolver ensures data is loaded before the component loads.
Alternatively, if you don't want to use *ngIf you could just use [ngClass]="{hideMe: allAsyncRequestsComplete}" to style the bit you don't want to show until loading is complete. CSS might be:
.hideMe {
visibility: hidden;
}
And set allAsyncRequestsComplete to true when loading is done.
You can use resolvers for the loading, then in app.component.ts, set your variable to true or false depending on the event:
navigationInterceptor(event: RouterEvent): void {
if (event instanceof NavigationStart) {
//true
}
if (event instanceof NavigationEnd) {
//false
}
// Set loading state to false in both of the below events to hide the spinner in case a request fails
if (event instanceof NavigationCancel) {
//false
}
if (event instanceof NavigationError) {
//false
}
}

Dynamic Form Creation in angular 6

I am trying to get started with creating a dynamic form in Angular 2, and I am using the setup from the Angular documentation . I didn't have any issues with their setup, which just hard codes the data in the service as apposed to an api call. My issue is that when I try to use an api call the values my dynamic form creation is failing.
The data is coming in from the api call successfully. I believe my issue is that [questions] is binding before the data is ready. Can someone tell me a better way to do this or please provide any suggestions to what I'm doing wrong please? Is there a way I could set the properties in the api first?
Here below my approcah looks like :
TS FILE
export class DynamicFormComponent implements OnInit {
#Input() questions: QuestionBase<any>[] = [];
form: FormGroup;
payLoad = '';
constructor(private qcs: QuestionControlService ,private dy :QuestionService) { }
ngOnInit() {
this.questions=this.dy.getDataFromService();
this.form = this.qcs.toFormGroup(this.questions);
}
onSubmit() {
this.payLoad = JSON.stringify(this.form.value);
}
}
HTML
<div>
<form (ngSubmit)="onSubmit()" [formGroup]="form" *ngIf="isFormReady">
<div *ngFor="let question of questions" class="form-row">
<app-question [question]="question" [form]="form"></app-question>
</div>
<div class="form-row">
<button type="submit" [disabled]="!form.valid">Save</button>
</div>
</form>
<div *ngIf="payLoad" class="form-row">
<strong>Saved the following values</strong><br>{{payLoad}}
</div>
</div>
##QUESTIONSERVICE.TS
getDataFromService(){
let questions: QuestionBase<any>[]=[];
this.auth.dynamicFormData().subscribe(
(result) => {
let data=result;
for(var i=0;i<data.length;i++){
questions.push(new TextboxQuestion(data[i]));
}
});
return questions.sort((a, b) => a.order - b.order);
}
}
RESULT DATA FROM API IS
[{"value":"Sireesha_0","key":"firstName_0","label":"First Name_0","required":true,"order":1,"controlType":"text"},{"value":"Sireesha_1","key":"firstName_1","label":"First Name_1","required":true,"order":1,"controlType":"text"}]
ERRORS
An error occurred: Cannot read property 'valid' of undefined
Error encountered in Error Interceptors TypeError: Cannot read property 'valid' of undefined at DynamicFormQuestionComponent.get [as isValid] (dynamic-form-question.component.ts:13)
I have changed my code like below and it's working good.
html
<form (ngSubmit)="onSubmit()" [formGroup]="form" *ngIf="isFormReady">
<div *ngFor="let question of questions" class="form-row">
<app-question [question]="question" [form]="form"></app-question>
</div>
<div class="form-row">
<button type="submit" [disabled]="!form.valid">Save</button>
</div>
</form>
ts
isFormReady:booelan;
ngOnInit() {
this.questions=this.dy.getDataFromService();
}
getDataFromService(){
let questions: QuestionBase<any>[]=[];
this.auth.dynamicFormData().subscribe(
(result) => {
let data=result;
for(var i=0;i<data.length;i++){
questions.push(new TextboxQuestion(data[i]));
}
});
this.form = this.qcs.toFormGroup(this.questions);
this.isFormReady=true;
return questions.sort((a, b) => a.order - b.order);
}

Working with custom components for input generates "No value accessor for form control with path X->0->Y"

I have a working form taking the following HTML markup. No errors or warnings.
<div class="input-element">
<div class="input-caption">Title</div>
<input type="text"
formControlName="targetField"
class="form-control">
</div>
I transformed it into a custom component, which also works, as shown below.
<app-input-text [info]="'Title'"
formControlName="targetField"
ngDefaultControl></app-input-text>
In my next view, I need to use FormArray as follows - still working code.
<div formArrayName="stuff">
<div *ngFor="let thing of form.controls.stuff.controls; let i = index;"
[formGroupName]=i>
<div class="input-element">
<div class="input-caption">Title</div>
<input type="text"
formControlName="targetField"
class="form-control">
</div>
</div>
</div>
Now, I expected that combining both (i.e. being able to use custom input component and being able to form array for components) would post no problem. However, the sample below doesn't work.
<div formArrayName="stuff">
<div *ngFor="let thing of form.controls.stuff.controls; let i = index;"
[formGroupName]=i>
<app-input-text [info]="'Title'"
formControlName="targetField"
class="col-sm-6"></app-input-text>
</div>
</div>
It generates the following error.
No value accessor for form control with path: 'stuff -> 0 -> targetField'
The custom component is design like this (although given that it works in the explicit markup example, I'm not sure if it's relevant information). The only (wild) guess I have might be that value isn't jacked into the form array field somehow.
export class InputTextComponent implements OnInit {
constructor() { this.value = new EventEmitter<string>(); }
#Input() info: string;
#Output() value: EventEmitter<string>;
onEdit(value: any): void { this.value.emit(value); }
}
The group and array creating in the current view is done like this (not sure if this is of any relevance neither, as it works for the explicit HTML markup case).
this.form = builder.group({
id: "",
stuff: builder.array([
builder.group({ targetField: "aaa" }),
builder.group({ targetField: "bbbb" }),
builder.group({ targetField: "cc" })
])
});
Is there a limitation in Angular in this regard that I'm not aware of? I'm rather sure there's not and that I'm just doing something fairly clever simply missing a tiny detail.
I do understand the error but I can't see how it relates to the code. The form can't find the 0th element in the array or that element has no field of that name. Since I do get to see a few rows, I know there must be a 0th element. Since I specified the name of the field, I know there is indeed such. What else am I missing?

ngif stays false, even though its value is right

My code looks like this:
component.ts
export class ViewScaleComponent implements OnInit {
private range$: Observable<number>;
constructor(private store: Store<AppState>) {}
ngOnInit() {
this.range$ = this.store.select(x => x.view.range);
}
}
component.html:
<div *ngIf="(range$ | async)">
here {{ range$ | async }}
</div>
The div remains commented in the DOM, an Angular Ghost.
If I remove ngif, the div appears, but the data interpolation fetches nothing (only here and the following whitespace appears).
If in ngOnInit() I do:
this.range$ = this.store.select(x => x.view.range).do(x => console.log(x));
console.log("on init " + this.range$);
then I get:
on init [object Object]
undefined
21
where the last two console logs come from the same line (do()). I wonder whether the first undefined is the reason for not displaying the di.
It's worth noting that sometimes it works(I see the div and the values ofrange$ are being updated as expected).
Any ideas?
PS: I read angular 2 ngIf with observable?, but the observable and async combination look pretty similar to mine.
You dont need the ngIf for this just put {{ range$ | async}}
Update
range:any ;
ngOnInit() {
this.store.select(x => x.view.range).subscribe(x => this.range = x);
}
Template
<div *ngif="range">
here {{ range }}
</div>

caused by: Cannot read property 'product_name' of undefined

When I try to load a html form component in my angular2 app, it will not read a property on one part of the form.
EXCEPTION: Uncaught (in promise): Error: Error in
http://localhost:3000/app/material/material-new.component.html:15:7
caused by: Cannot read property 'product_name' of undefined
I have another component that is identical bar the fields and does not encounter this problem when loaded. Components match and I am going mad about this.
Why does it not read that property of 'product_name' .
Heres the code.
Create Material
<div class="card container form-container">
<div class="row">
<div class="col-md-12">
<form (ngSubmit)="createMaterial(material)" #materialForm="ngForm" >
<div class="form-group">
<label class="label-text" for="product_name">Product
Name</label>
<input type="text"
class="form-control"
id="product_name"
placeholder="Product name"
required
name="product_name"
#product_name='ngModel'
[(ngModel)]="material.product_name">
<div [hidden]="product_name.valid || product_name.pristine">
Input product name
</div>
</div>
import { Component } from '#angular/core';
import { Material } from './material';
import { MaterialService } from './material.service';
import { Observable } from 'rxjs/Rx';
#Component({
moduleId: module.id,
selector: 'material-new',
templateUrl: 'material-new.component.html',
styleUrls: ['material-new.component.css'],
providers: [ MaterialService ]
})
export class MaterialNewComponent {
material : Material;
submitted : boolean = false;
constructor(
private materialService : MaterialService
) {}
createMaterial( material : Material) {
this.submitted = true;
this.materialService.createMaterial(material)
.subscribe(
data => { return true },
error => { console.log("error saving material")
return Observable.throw(error);
}
)
}
}
You error points to [(ngModel)]="material.product_name"
Your material object is undefined, because you have not initialized it. So all you need to do, is to initialize your Object.
So change:
material : Material;
to
material : Material = <Material>{};
and it will no longer be undefined.
You should use async pipe to unwrap the Observable because it does the job of subscribing and unsubscribing automatically.
What you'll need to do:
TS Code:
material: Observable<Material>;
// In createMaterial
this.material = this.materialService.createMaterial(material);
Template:
[(ngModel)]="(material | async)?.product_name"
The ? will check if material | async is undefined.