Angular 2: populate FormBuilder with data from http - javascript

I get my data from http with rjsx in component (let name it customer).
Then i'm using inner component in customer:
<customer>
<customer-form [customer]="customer"></customer-form>
</customer>
<!-- [customer]="customer" // here is data from http -->
and in customer-form i have:
#Input() customer:ICustomer;
complexForm : FormGroup;
constructor(fb: FormBuilder) {
this.complexForm = fb.group({
'name': [this.customer['name'], Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(255)])]
});
}
but i get:
Cannot read property 'name' of undefined
TypeError: Cannot read property 'name' of undefined
if i understood correctly: it's due to the fact that constructor is called, but data isn't fetched yet from http, so customer is empty. But how to fix this?
upd: my http data get:
getCustomer(id) {
this.customerService.getCustomer(id)
.subscribe(
customer => this.customer = customer,
error => this.errorMessage = <any>error);
}
----
#Injectable()
export class CustomerService {
private customersUrl = 'api/customer';
constructor (private http: Http) {}
getCustomers (): Observable<ICustomer[]> {
return this.http.get(this.customersUrl)
.map(this.extractData)
.catch(this.handleError);
}
getCustomer (id): Observable<ICustomer> {
return this.http.get(this.customersUrl + '/' + id)
.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, 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);
}
}

as #Bhushan Gadekar stated, you are accessing customer when it has not been initialized.
There are multiple way to handle this correctly :
Using a setter:
#Input("customer")
set _customer(c:ICustomer){
this.customer=c;
this.complexForm.get("name").setValue(c.name,{onlySelf:true});
}
customer:ICustomer;
complexForm : FormGroup;
constructor(fb: FormBuilder) {
this.complexForm = fb.group({
'name': [null, Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(255)])]
});
}
Using an Observable
Here, the customer needs to be an Observable of ICustomer
#Input() customer:Observable<ICustomer>;
complexForm : FormGroup;
constructor(fb: FormBuilder) {
this.complexForm = fb.group({
'name': [this.customer['name'], Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(255)])]
});
}
ngOnInit(){
this.customer.map(c=>this.complexForm.get("name").setValue(c.name,{onlySelf:true}))
.subscribe();
}
Mixing both :
#Input("customer")
set _customer(c:ICustomer){
this.customer.next(c);
}
customer=New Subject<ICustomer>();
complexForm : FormGroup;
constructor(fb: FormBuilder) {
this.complexForm = fb.group({
'name': [null, Validators.compose([Validators.required, Validators.minLength(3), Validators.maxLength(255)])]
});
}
ngOnInit(){
this.customer.map(c=>this.complexForm.get("name").setValue(c.name,{onlySelf:true}))
.subscribe();
}
Case for multiple properties :
If you don't want to write every form update one by one, and if your form's field names are the same as your Object you can loop over customer properties:
Object.keys(customer).forEach(k=>{
let control = this.complexForm.get(k);
if(control)
control.setValue(customer[k],{onlySelf:true});
});
Note that this code will work only if your form's controls are named the same way as customer's properties are. If not, you may need to make a hash mapping customer properties name to formControls name.
Important point:
Yous should never access inputs from the constructor as they are not populated yet, all inputs should get populated (at least the synchronous ones) just before the ngOnInit hook. Take a look at the Lifecycle hooks documentation

I can see that you are trying to access customer object when it is not populated.
Issue here is that http call takes some time to be resolved.thus, your view is trying to access customer object even when it is undefined.
try this:
<customer *ngIf="customer">
<customer-form [customer]="customer"></customer-form>
</customer>
Though the way you are accessing name property is also not good.
Best approach is to create a customer model and use your property as className.propertyName
Hoe this helps.

Instead of ngOnInit , try ngAfterViewInit

do not use subscribe in component.ts and add async pipe in component.html, like so:
<customer-form [customer]="customer | async"></customer-form>

Related

Angular pushing an array of an user object into a subject

I'm developping a single app and at the moment the only good behavior is that I'm getting an user from an API with HttpClient method.
The method is store in a service.
Getting the user is a success but now I want to get a specific array from that user to re-use it by my will.
Should I make another service since this value will be use in 2 components ?
How should I procced to get this array in a var ?
Exemple of user object :
{
firstName: '',
lastName: '',
arrayIWant: []
}
My user is in a subject and here is the way I use it in a component
user: User;
userSubscription: Subscription;
constructor(
public userService: UserService
) {
}
ngOnInit() {
this.userSubscription = this.userService.userSubject.subscribe(
(user: User) => {
this.user = user;
}
);
this.userService.getSingleUserFromServer();
this.userService.emitUser();
}
ngOnDestroy() {
this.userSubscription.unsubscribe();
}
Should I put this code in every component where I want to use the user or is there a way to definie globaly the user ?
You can use a BehaviourSubject which will hold the last value of whatever that service populates the userSubject with
public userSubject: BehaviourSubject<User> = new BehaviourSubject(null);
getSingleUserFromServer(): void {
//get your user from http
userSubject.next(result);
}
In you HTML you can use the async pipe to display the values of the inner array you want. Or just use it in your component by subscribing to the last emission of the behaviourSubject
//X.Component
public subscriptionKiller: Subject<void> = new Subject();
ngOnInit(): void {
this.userService.userSubject
.pipe(takeUntil(this.subscriptionKiller))
.subscribe((lastUser: User) => {
someMethod(this.userService.userSubject.value.arrayIWant);
}
}
ngOnDestroy(): void {
this.subscriptionKiller.next()
}

Shared service with BehaviorSubject not emitting correct value

I am trying to implement a shared service for managing Roles on my app, with an Observable so that, from other components, you can either change the current role and be notified when it changes. The problem I have is that when I publish a new value through the service, the components that subscribe to the observable always recieve the same value (the initial one). Then, I never receive the new role number and I can't update the component state.
Apparently
I have the following set of components:
RolesService: The shared Service, which manages role change, gets the available roles from the user token, manages persistence of the current role for the logged in user. It uses localStorage to persist the role index. It receives changes
HeaderComponent: This is an example of a component receiving changes for the role change, because it needs to update the title of the user. It subscribes to the observable and changes the title accordingly
EscullRolComponent: And this is an example of a component that changes the role the user is currently using (by action of the user, of course). It has some buttons and sends to the service the new index.
Here is the relevant code for this issue:
// RolesService file
#Injectable()
export class RolesService {
private _currentRole: BehaviorSubject<Rol> = new BehaviorSubject(null);
currentRole = this._currentRole.asObservable();
private get currentIndex(): number {
const ras = localStorage.getItem('current_role');
// console.log("Guardat aixo: ", ras);
if (ras === '' || ras === null || ras === 'NaN' || ras === '-1' || parseInt(ras, 10) === NaN) {
return 0;
} else {
return parseInt(ras, 10);
}
}
private set currentIndex(val) {
localStorage.setItem('current_role', val.toString());
}
currentToken: NbAuthJWTToken;
constructor(private authService: NbAuthService,
private http: HttpClient) {
// console.log(this.currentRole);
this.authService.getToken().subscribe((token: NbAuthJWTToken) => {
if (token.isValid()) {
this.currentToken = token;
console.log("Executing token change");
this.setRolActual(0);
}
});
}
protected publishCurrentRol(i: number): void {
console.log("Publishing rol id: ", i); // LOG 1
this._currentRole.next(this.getUserRoles()[i]);
}
setRolActual(i: number) {
this.publishCurrentRol(i);
this.currentIndex = i;
}
}
The following is the component the user has to change the role, and that calls the service with the new role.
#Component({
templateUrl: 'escull-rol.component.html',
styleUrls: ['escull-rol.component.scss'],
})
export class EscullRolComponent {
rols: Array<Rol> = [];
actual: number;
constructor( private rolesService: RolesService,
private route: ActivatedRoute,
private router: Router,
private location: Location ) {
this.rols = this.rolesService.getUserRoles();
this.actual = this.rolesService.getRolActualIndex();
}
buttonRolClicked(index: number) {
this.rolesService.setRolActual(index);
this.router.navigate(['inici']);
// console.log('Boto del rol ' + index + ' clicat');
}
}
And here the header, which changes its state depending on the role:
#Component({
selector: 'ngx-header',
styleUrls: ['./header.component.scss'],
templateUrl: './header.component.html',
})
export class HeaderComponent implements OnInit {
#Input() position = 'normal';
user: any = {};
picture: string;
client: stream.Client;
logoPath = '';
logoEra = '';
rol: string;
ids: Array<string>;
constructor(private sidebarService: NbSidebarService,
/* ... more injections .. */
private imatgesService: ImatgesService,
private notificacionsService: NotificacionsService) {
this.logoEra = 'assets/images/logoEra.png';
this.authService.onTokenChange()
.subscribe((token: NbAuthJWTToken) => {
if (token.isValid()) {
if (token.getValue()) {
this.user = token.getPayload(); // Posem les dades del token a l'objecte user
// console.log('token changed, changing user in header', token);
}
}
}, error => {
console.error('Error en carregar el token en el header');
throw error;
});
this.rolesService.currentRole.subscribe((rol: Rol) => {
// This subscription here should receive the change from the service
if(rol) {
console.log("Changing rol on header to ", rol.getIndex()); // LOG 2
this.rol = rol.getTitle();
this.ids = rol.getRolIDs();
}
});
this.imatgesService.getProfileImagePath().subscribe((path: string) => {
this.picture = path;
}, (err) => {
this.picture = '';
});
}
}
The behaviour that I'm seeing is, the EscullRol component calling the setRolActual(id) method with the new id, and then the service calling its internal method publishCurrentRole(id) with the same id, so at LOG 1 I can see the expected outoput. But then immediately next I can see the output form LOG 2 at the Header Component with the wrong id, which is always the number that we had initially saved at the localStorage when the app started up.
I don't really know if the issue is with how I use the observables, with the service-component communication model or with how components and observables are initailsed and treated in angular.
Few thing to try
First make your service as a singleton using
#Injectable({ providedIn: "root" })
Improvement
Also, make sure that the service is not provided on child modules, as that would create their own instance and it wouldn't be a singleton anymore. If there are more than one instance of the service, the Subject-Observer pattern will not work across all the app.
Then this code
currentRole = this._currentRole.asObservable();
You should create a function to return the data not defined as an variable like
getCurrentRole() {
return this._currentRole.asObservable();
}

Angular HttpClient get, wrong this object

in my Angular App i make a simple call to a node.js server. the HttpClient "get"
function returns the right answer. This answer I want to store in a variable of my component "interfaces". But in the "subscribe" function of the get request my "this" pointer doesn't point to my component. Instead it tells me that it is of type "SafeSubscriber". Any call to my member "interfaces" lead to the following error:
TypeError: Cannot read property 'interfaces' of undefined
export class SettingsComponent implements OnInit {
public interfaces : string[];
constructor(private http: HttpClient) {
this.interfaces = [];
this.interfaces.push("huhu");
}
ngOnInit() : void {
this.http.get('http://localhost:3000/settings/interfaces').subscribe((data) => {
// Read the result field from the JSON response.
console.log(data);
this.interfaces.push("xxx");
Object.keys(data).forEach(function(k) {
console.log(k);
this.interfaces.push("xxx");
});
}),
err => {
console.log("error " + err);
};
}
}
As you can see I also tried to enter some values manually into the array just to make sure, that not the server response is causing the problem.
Any help is appreciated.
I used this code as a blueprint which is from:
https://angular.io/guide/http
#Component(...)
export class MyComponent implements OnInit {
results: string[];
// Inject HttpClient into your component or service.
constructor(private http: HttpClient) {}
ngOnInit(): void {
// Make the HTTP request:
this.http.get('/api/items').subscribe(data => {
// Read the result field from the JSON response.
this.results = data['results'];
});
}
}
You're losing reference to the correct this in this statement:
Object.keys(data).forEach(function(k) {..})
Inside the function block code this refers to the calling context , which is the subscribe method itself, that's why interfaces is undefined, since it's not a property of the subscribe method.
You can change the function for a lambda en it should be fine:
Object.keys(data).forEach((k) => {..})

Why is the Observable and subscriber for an array not working?

I am trying to write a simple error handling service. It can receive errors and add them in an array but for some reason, it is not working.
errorhandling.service
#Injectable()
export class ErrorHandlingService {
private errors: Array<ErrorMessage>;
constructor() {
this.errors = new Array<ErrorMessage>();
}
getErrors(): Observable<Array<ErrorMessage>>{
return Observable.of(this.errors);
}
handleError(error: Response, errorText: string) {
let errorMessage = this.createErrorMessage(error);
errorMessage.displayText = errorText;
this.errors.push(errorMessage);
}
private createErrorMessage(error: Response): ErrorMessage {
let errorMessage: ErrorMessage = new ErrorMessage();
errorMessage.errorType = error.type;
errorMessage.statusCode = error.status;
return errorMessage;
}
}
export class ErrorMessage {
statusCode: number;
displayText: string;
errorType: ResponseType;
}
app.component.ts
export class AppComponent implements OnInit{
errorMessage: Message[] = [];
constructor(private authService: AuthService, private renderer: Renderer, private errorhandlingService: ErrorHandlingService) {
localStorage.removeItem(AppConstants.authenticationLocalStorageKey);
}
ngOnInit(): void {
this.errorhandlingService.getErrors().subscribe(errorMessages =>{
let errorMessage: ErrorMessage = errorMessages.pop();
console.log(errorMessage);
this.errorMessage = errorMessage ? [{ severity: 'error', summary: '', detail: errorMessage.displayText }] : [];
});
}
onDeactivate() {
//scroll to top of page after routing
this.renderer.setElementProperty(document.body, "scrollTop", 0);
}
}
app.component.html
<div class="row">
<div class="col-md-1"></div>
<div class="col-md-10 well center-text">
<p-messages [value]="errorMessage"></p-messages>
<router-outlet (deactivate)="onDeactivate()"></router-outlet>
</div>
<div class="col-md-1"></div>
</div>
The code below is in another component which triggers the errorhandling service method.
businessArea.component.ts
this.businessAreaService.getBusinessAreaById(id)
.subscribe(businessArea => {
this.model = businessArea;
},
error => this.errorHandlingService.handleError(error, 'Could not load Business Area'));
Edit: I tried many things like Observable, but it didn't work as well. I am not sure what is the subject and how to use it but it seems that Observable makes sense in my scenario. Any useful link will help as well? I can breakpoint and I can see it hit the method and then it pushes the Error in the array but subscribe on app.component is never called.
expected behaviour:
When this.businessAreaService.getBusinessAreaById is called in an error case it calls errorHandlingService.handleError which logs the error in an array in errorHandlingService by calling the method this.errorHandlingService.handleError. Now I have a subscriber in app.component which should be called when an error is added to an array, so that I can display error in a div.
What is not working:
When an element/error is added to the array in errorhandling service, the subscriber on the app.component.ts is not called. Shouldn't adding element in array trigger subscriber? It is only called the first time when ngOnit is called. After any subsequent errors subscriber is not called. Although I can see the error being pushed in the array.
You can use Subject to achieve it, update the code inside your service class like below, you don't have to modify anything else except for the service class
import { Subject } from 'rxjs';
#Injectable()
export class ErrorHandlingService {
private errors: Array<ErrorMessage>;
private broadcaster = new Subject<Array<ErrorMessage>>();
constructor() {
this.errors = new Array<ErrorMessage>();
}
getErrors(): Subject<Array<ErrorMessage>>{
return this.broadcaster;
}
handleError(error: Response, errorText: string) {
let errorMessage = this.createErrorMessage(error);
errorMessage.displayText = errorText;
this.errors.push(errorMessage);
// letting all subscribers know of the new change
this.broadcaster.next(this.errors);
}
private createErrorMessage(error: Response): ErrorMessage {
let errorMessage: ErrorMessage = new ErrorMessage();
errorMessage.errorType = error.type;
errorMessage.statusCode = error.status;
return errorMessage;
}
}
export class ErrorMessage {
statusCode: number;
displayText: string;
errorType: ResponseType;
}

Change #Input field of #Component after sending an Http Request (Angular2)

I am working on an Angular2 application and one of the #Components has a button that when clicked will send a post request to my server which will either respond with an Ok(string) or a BadRequest(string).
I am having trouble updating an #Input field of one of my #Components after getting the answer from the server.
Below are simplified version of some of my classes.
My Component class
#Component({
moduleId: module.id,
selector: 'model-comp',
templateUrl: './model.component.html',
styleUrls: ['./model.component.css']
})
export class MyComponent{
#Input() model: Model;
#Output() emitter: EventEmitter<Model> = new EventEmitter<Model>();
public constructor(private service: MyService){}
public toggle(): void {
this.service.send(model.id, model.name){
.subscribe(
result => this.onSuccess(result)),
error => this.onError(error),
() => this.onComplete());
}
public onSuccess(result: string): void {
if(result.inculdes("Some Text")) this.model.flag = true;
else this.model.flag = false;
this.emitter.emit(this.model);
}
public onError(error: any): void {
//notification using bootstrap-notify
}
public onComplete(): void {
//currently empty
}
}
My Service class
export class MyService{
public send(id: string, name: string){
return <Observable<string>>this.http
.post('url', new Dto(id, name))
.map(result => this.getData<string>(result))
.catch(this.catchBadResponse);
}
private getData<E>(result: Response): E {
//checking if result.status is ok
var body = result.json ? res.json(): null;
return <E>(body || {});
}
private catchBadRespomse: (error: any) => Observable<any> = (error: any) => {
var response = <Response>error;
var json = response.json();
var msg = json.Message;
var errormsg = json?
(json.error ? json.error: JSON.stringify(msg?msg:json)) :
(response.statusText || 'Error?');
return Obserable.of(errormsg);
}
}
Template of MyComponent
<button (click)="toggle()"
[ngClass]="{'class1': true, 'class2': model.flag}">Text</button>
Template of Parent Component
<div *ngFor="let model of getList()">
<model-comp [model]="model" (emitter)="onEmit($event)"></model-comp>
</div>
The onEmit Function
onEmit(evt: any): void{
if(evt instanceof Model){
var evtModel = evt as Model;
this.list.find(search => search.id == evtModel.id)
.isFav = evtModel.isFav;
}
}
The problem is that even though I post my data and receive the response, The property flag of my model does not change.
I think that the click event reloads the component thus removing the observers of the EventEmitter.
So is there any way to cancel the reload, not lose the observers of the EventEmitter or any other way to update the root object or the element class?
update (see comments below the question)
If getList() (what *ngFor binds to) returns a new list every time it is called, *ngFor will be permanently busy rerendering the items because change detection will cause getList() being called again and again.
Binding to a function that returns a new object or array every time it's called directly will cause serious issues like exceptions and dramatic performance degredation.
Using method/function calls in the view is strongly discouraged in general. Rather assign the list to a field and bind to that field instead of the method.
ngOnInit() is fine for initializing the list but also any event handler for initializing or updating the list.
original
If you modify the model value that you got passed in from the parent, then the parent also sees the change. Emitting the value as an event is probably redundant.
I guess you are modifying list (from <div *ngFor="let model of list">) in onEmit() which then causes *ngFor to rerender the list.
I don't think you should change #input property from within the component.
it suppose to listen and act to changes from the parent component.
MyComponent.ts
export class MyComponent{
#Input() model: Model;
//#Output() emitter: EventEmitter<Model> = new EventEmitter<Model>();
public constructor(private service: MyService){}
public toggle(): void {
this.service.send(model.id, model.name){
.subscribe(
result => this.onSuccess(result)),
error => this.onError(error),
() => this.onComplete());
}
public onSuccess(result: string): void {
if(result.inculdes("Some Text")) this.model.flag = true;
else this.model.flag = false;
//this.emitter.emit(this.model);
this.service.emitter.next(false);
}
public onError(error: any): void {
//notification using bootstrap-notify
}
public onComplete(): void {
//currently empty
}
}
Service
#Injectable // important
export class MyService{
public emitter: Subject<any> = new Subject();
public send(id: string, name: string){
return <Observable<string>>this.http
.post('url', new Dto(id, name))
.map(result => this.getData<string>(result))
.catch(this.catchBadResponse);
}
private getData<E>(result: Response): E {
//checking if result.status is ok
var body = result.json ? res.json(): null;
return <E>(body || {});
}
private catchBadRespomse: (error: any) => Observable<any> = (error: any) => {
var response = <Response>error;
var json = response.json();
var msg = json.Message;
var errormsg = json?
(json.error ? json.error: JSON.stringify(msg?msg:json)) :
(response.statusText || 'Error?');
return Obserable.of(errormsg);
}
}
Now you can listen to Service.emitter anywhere in app

Categories