Chris Rutherford
Chris Rutherford

Reputation: 1672

@Input Complex object, initialized through ngOnInit, throwing error Cannot Read Property of undefined

Passing a parameter to a child component in the parent component:

<app-conf-name
    [inputName]='person.name'
    (updatedNameEvent)='updateName($event)'>
</app-conf-name>

Defined in the component TS file

@Input() inputName: Names;
@Output() updatedNameEvent: EventEmitter<Names> = new EventEmitter();
constructor() { }
editName: Names = new Names();

ngOnInit() {
  this.editName = this.inputName;
}

In the class, the Names type sets a default value for the correctPerson field.

export class Names {
  indivId: number;
  firstName: string;
  prefName: string;
  lastName: string;
  suffix: string;
  updated: boolean = false;
  correctPerson: boolean = false;
  correctAsIs: boolean = false;
  correctionDate: Date;
  addedDate: Date;
}

Why am I getting this error:

ERROR TypeError: Cannot read property 'correctPerson' of undefined
    at Object.View_ConfNameComponent_0._co [as updateDirectives] (ConfNameComponent.html:9)
    at Object.debugUpdateDirectives [as updateDirectives] (core.es5.js:13056)
    at checkAndUpdateView (core.es5.js:12236)
    at callViewAction (core.es5.js:12601)
    at execComponentViewsAction (core.es5.js:12533)
    at checkAndUpdateView (core.es5.js:12242)
    at callViewAction (core.es5.js:12601)
    at execComponentViewsAction (core.es5.js:12533)
    at checkAndUpdateView (core.es5.js:12242)
    at callViewAction (core.es5.js:12601)

The Object person is initialized from a service that is called in the parent component.

Parent Component

person: Person = new Person();
constructor(private peopleService: PeopleService) { }
state: string = 'one';
ngOnInit() {
    this.peopleService.getPersonById(this.Id).subscribe((data)=>{
      this.person = data;
      console.log(this.person);
      this.person.name.correctPerson = false;
      this.person.name.correctAsIs = false;
      this.person.email.correctAsIs = false;
      this.person.phone.correctAsIs = false;
      this.person.address.correctAsIs = false;
    });
}

Note that I've initialized the property that's throwing the error a second time via an assignment.

the original code for the child component used the input variable as the working variable.

After Several attempts to answer this question and get the full project working, I've gotten to a point where the final child component is the last one throwing errors after putting the ngIf's everywhere.

here's a link to the full GitHub Repo

Upvotes: 1

Views: 2223

Answers (3)

JeanPaul A.
JeanPaul A.

Reputation: 3724

The problem with the above code is that the call to the server is asynchronous. This means that at the time the child component is created it does not necessarily mean that the name is defined.

The flow of data is as follows

  • The parent component is created, with an empty People object. (thus not fail when people.name is called, because people is never undefined).
  • Upon init, the parent component makes the asynch chall.
  • Following init the child component is created (it won't wait for the asynchronous call to finish).
  • The child component would fail, because name on the parent object (people) is undefined (probably being read in the child component's html).

There are two fixes to your problem. The first is by adding ngIf on your child component tag as below

<app-conf-name 
    *ngIf='person.name' [inputName]='person.name'
    (updatedNameEvent)='updateName($event)'>
</app-conf-name>

The second solution, is to make use of a resolve guard, which resolves the data from your service, before the parent component is created.

In your route definition

{
  path: 'your-path', component: YourParentComponent,
  resolve: {
    person: PersonResolveGuard
  }
}

Your resolve guard

@Injectable()
export class BlockingResolveGuard implements Resolve<Person> {

    constructor(private peopleService: PeopleService) {}

    resolve(route: ActivatedRouteSnapshot):Observable<Person> {
       // Call to service
    }
}

In your parent component, inject the current activated route

constructor(private route:ActivatedRoute, private peopleService: PeopleService) { }

and in your ngOnInit()

ngOnInit() {
    this.route.data.resolve((data:{person:Person}) => {
       this.person = data.person;
    }
}

This angular doc: gives a detail explanation how resolve guards (and other guards work) https://angular.io/guide/router#resolve-guard

Note that with the above solution, you need to resolve the id of the person you're retrieving the data for from the route parameters using the activatedRouterSnapshot in the resolve guard.

Upvotes: 0

Reactgular
Reactgular

Reputation: 54751

You are doing two things here. Lazy loading data and also loading that data in the parent ngOnInit. Therefore, you have to assume it's possible for the @Input() binding in the child to be undefined.

This is because the @ViewChild properties are set before ngOnInit is executed, but @ContentChild are set afterwards. This means that the template for the parent component is rendered to a view before your data is loaded, and that template is passing undefined to the @Input() binding on the child.

Note: I hope the above is accurate. Haven't had my morning coffee yet.

Either way, you should always support missing bindings for components.

@Input() inputName: Names;
@Output() updatedNameEvent: EventEmitter<Names> = new EventEmitter();
constructor() { }
editName: Names;

ngOnInit() {
  this.updateEditName();
}

ngOnChanges(changes: SimpleChanges) {
   if('inputName' in changes) {
       this.updateEditName();
   }
}

private updateEditName() {
      this.editName = this.inputName || new Names();
}

Upvotes: 1

tlt
tlt

Reputation: 15211

You should not access @Input() parameters in OnInit but in OnChanges instead. Its an Observable and there is no guarantee it will resolve till the moment OnInit fires.

For details, take a look here: Angular 2/4 @Input only moves intrinsics - not lists or objects

Upvotes: 3

Related Questions