0

UPDATE I've put a return after a console log and determined that the issue isn't the Promise -- it's reading that just fine. At the line I highlight in the code below, this.contentItems = cpl.ary as ContentItem[]; is where the error is occuring. I've verified that cpl is an object with one property, ary, which is an array of 4 items. It displays in the console log fine. But when I try to get the ary property as ContentItem[] it comes up with null and tries to assign it to my empty Content[] on the left-hand side. So this is purely a casting or other type assignment issue. I'll change the subject of this question once I've found an answer.

My problem is that this.contentItems gets set to null in the then methods in content-display.component.ts.

The JSON I'm consuming is this:

{ "ary":[
  {
    "id": "1",
    "name": "Test Project",
    "nextDate": "2016-12-01T16:00:00",
    "dateDescription": "ends",
    "contentType": "project",
    "link": "http://google.com",
    "topics": ["1"]
  },
  {
    "id": "3",
    "name": "Dummy Article",
    "nextDate": "2016-12-21T00:00:00",
    "dateDescription": "expires",
    "contentType": "article",
    "link": "http://msn.com",
    "topics": ["2"]
  }
]}

In my content-item.service.ts:

getContentItems(): Promise<ContentPayload> {
  if( window.location.hostname === "localhost" )
    return Promise.resolve( CONTENTITEMS );
  else
    return this.http.get( this.contentUrl )
                  .toPromise()
                  .then( (response) => { return response.json() } )
                  .then( (data) => { return data as ContentPayload })
                  .catch( this.handleError );
}

And back in my content-display.component.ts

private contentItems: ContentItem[] = [];

getContentItems(): void {
  this.contentItemService.getContentItems()
    .then( (contentPayload) => this.getArrayFromPayload )
    .then( this.getSelectOptions );
}

getArrayFromPayload( cpl: ContentPayload ): void {
  this.contentItems = cpl["ary"];
  /********** above, this.contentItems is set to null ************/
  this.filteredItems = this.contentItems;
}

ContentPayload:

export class ContentPayload {
  ary: any[];
}

ContentItem:

export class ContentItem {
  id: string;
  name: string;
  nextDate: Date;
  dateDescription: string;
  contentType: string;
  link: string;
  isClicked?: boolean = false;
  topics: string[];
}

This one is included for completeness. What it does isn't important, the error occurs above.

getSelectOptions( ): void {
  // return TOPICMAP;
  var topicMap: Topic[] = TOPICMAP;
  var filteredTopicMap: Topic[] = [];
  var uniqueValues: string[] = this.makeContentSet( this.getTopicIDs( this.contentItems ) );
  for( var i = 0; i < uniqueValues.length; i++ ) {
    filteredTopicMap.push( this.getTopicWithID( topicMap, uniqueValues[i] ) );
  }
  this.selectOptions = filteredTopicMap;
}

And my template for this component (inconsistencies in style and use of "this" will be cleaned up eventually. And did you know iPads don't have back-ticks?):

template: `
<h2>{{title}}</h2>
<div>Maximum of 20 items</div>
<div>Order:
  <span [class.clicked]="latestOldest==='latest'" (click)="mySort('latest')">latest first</span>
  |
  <span [class.clicked]="latestOldest==='oldest'" (click)="mySort('oldest');">oldest first</span>
</div>
<div>
  Filters:
  <ng-select
    [options] = "this.selectOptions"
    [multiple] = "this.showMultiple"
    placeholder = "Select topics"
    [allowClear] = "true"
    theme = "default"
    (selected) = "onSelected( $event )"
    (deselected) = "onDeselected( $event )">
  </ng-select>
<ul>
  <li *ngFor="let contentItem of filteredItems"
      (click)="onClick(contentItem)"
      class="{{contentItem.contentType}}"
      [class.clicked]="contentItem.isClicked">
    <h3>{{contentItem.name}}</h3>
    <div>{{ contentItem.contentType | uppercase }} {{ contentItem.dateDescription}}
      {{ contentItem.nextDate.toGMTString() | date:'mediumDate' }}
      {{ (contentItem.contentType !== "article")? (contentItem.nextDate | date:'shortTime'): '' }}
    </div>
  </li>
</ul>
</div>
`
Sangwin Gawande
  • 7,658
  • 8
  • 48
  • 66
Colin Mac
  • 3
  • 2
  • I tried mocking out the http call with a mock ContentPayload object, but get the same result. – Colin Mac Oct 21 '16 at 20:44
  • try changing ` .then( (contentPayload) => this.getArrayFromPayload )` to ` .then( this.getArrayFromPayload )` – bassxzero Oct 21 '16 at 20:53
  • thanks, but still getting 'EXCEPTION: Uncaught (in promise): TypeError: Cannot set property 'contentItems' of null' – Colin Mac Oct 21 '16 at 20:59
  • http://stackoverflow.com/questions/36492169/uncaught-exception-in-promise-when-when-trying-to-use-nested-components Looks like the template for your component might not be valid html. Show template? – bassxzero Oct 21 '16 at 21:08
  • You are missing a closing `` for your open div on line 9 – bassxzero Oct 24 '16 at 12:00
  • Thanks, bassxzero, I've fixed that, but that didn't help. See my update at the top of the post, it seems to be an error with the way I'm handling the payload. `cpl` appears as the JSON object, but `cpl.ary` doesn't find the array of `ContentItem[]`. The console, however, show that that value is [Object, Object, Object, Object], which should be correct. – Colin Mac Oct 24 '16 at 13:43
  • Yeah I don't think `return data as ContentPayload` does what you want it to. Try explicitly creating and settings the properties of a `ContentPayLoad` instead of trying to cast a JSON object. – bassxzero Oct 24 '16 at 16:29
  • I did as you suggested, but I think the problem in this particular case was that `this` in the function that was called in the Promise's `then` was not equivalent to the component. Not sure what it stood for, but something else. So I changed `.then( (contentPayload) => this.getArrayFromPayload )`, which assigned this.contentItems within that function, into `.then((contentPayload)=>this.contentItems = this.getArrayFromPayload)`, and that worked. – Colin Mac Oct 24 '16 at 20:11

1 Answers1

1

Your problem is caused by not handling this correctly. There are many questions here on exactly this topic.

When you write the following

getContentItems(): void {
  this.contentItemService.getContentItems()
    .then( (contentPayload) => this.getArrayFromPayload )
    .then( this.getSelectOptions );
}

then you are basically pulling out the function of the object it was defined on (from the component instance in your case). The function still refers to some this but it won't get called with your component as this any more, that's what causes your problem.

The mentioned way in the comments of using .then( this.getArrayFromPayload ) also doesn't work, as this also changes the context when the function is called.

There are several ways of fixing this. You could bind() the function passed to the then method to the this of the caller context like so

.then(this.getArrayFromPayload.bind(this))

Doing so sets the currently used this to the function such that it is preserved when the function is called. (It basically returns a new function that has the correct this reference)

Another way would be to use an arrow function which doesn't have an own this context and thus refers to the outer this.

getArrayFromPayload = (cpl: ContentPayload) => {
  this.contentItems = cpl["ary"];
  this.filteredItems = this.contentItems;
}

Doing so allows you to call it just like .then( this.getArrayFromPayload ).

For details on the underlying concepts, see this great answer covering the fundamentals of this in JavaScript: How to access the correct `this` context inside a callback?

Community
  • 1
  • 1
Andreas Jägle
  • 11,632
  • 3
  • 31
  • 31