2
votes

I'm using the contentful js SDK to fetch data in a service. The SDK provides a method to retrieve entries from a promise and also parse those entries. Since I would like to use observables, I am returning the promise as an observable and then transforming from there.

In my home component, I am then calling the contentfulService OnInit and unwrapping the observable in the template using the async pipe.

My problem:
When the home component loads, the template is not there even though the service has fetched the data successfully. Now, if I interact with the DOM (click, hover) on the page, the template will instantly appear. Why is this not just loading asynchronously on page load? How can I fix this?

An example .gif showing the behavior.

contentful.service.ts

import { Injectable } from '@angular/core';    
import { Observable, Subject } from 'rxjs/Rx';    
import { Service } from '../models/service.model';
import * as contentful from 'contentful';


@Injectable()
export class ContentfulService {

  client: any;

  services: Service[];
  service: Service;    

  constructor() {    
    this.client = contentful.createClient({
      space: SPACE_ID,
      accessToken: API_KEY
    });
  }   


  loadServiceEntries(): Observable<Service[]> {

    let contentType = 'service';
    let selectParams = 'fields';

    return this.getEntriesByContentType(contentType, selectParams)
      .take(1)          
      .map(entries => {
        this.services = [];

        let parsedEntries = this.parseEntries(entries);

        parsedEntries.items.forEach(entry => {
          this.service = entry.fields;
          this.services.push(this.service);
        });

        this.sortAlpha(this.services, 'serviceTitle');
        return this.services;
      })          
      .publishReplay(1)
      .refCount();

  }


  parseEntries(data) {
    return this.client.parseEntries(data);
  }


  getEntriesByContentType(contentType, selectParam) {

    const subject = new Subject();

    this.client.getEntries({
      'content_type': contentType,
      'select': selectParam
    })
      .then(
      data => {
        subject.next(data);
        subject.complete();
      },
      err => {
        subject.error(err);
        subject.complete();
      }
      );

    return subject.asObservable();
  }


  sortAlpha(objArray: Array<any>, property: string) {
    objArray.sort(function (a, b) {
      let textA = a[property].toUpperCase();
      let textB = b[property].toUpperCase();

      return (textA < textB) ? -1 : (textA > textB) ? 1 : 0;
    });
  }


}

home.component.ts

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import { ContentfulService } from '../shared/services/contentful.service';
import { Service } from '../shared/models/service.model';    

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {

  service: Service;      
  services: Service[];
  services$: Observable<Service[]>;

  constructor(
    private contentfulService: ContentfulService,
  ) {

  }    

  ngOnInit() {       

    this.services$ = this.contentfulService.loadServiceEntries();

    this.services$.subscribe(
      () => console.log('services loaded'),
      console.error
    );    

  }; 


}

home.component.html

...
<section class="bg-faded">
  <div class="container">
    <div class="row">
      <div class="card-deck">
        <div class="col-md-4 mb-4" *ngFor="let service of services$ | async">
          <div class="card card-inverse text-center">
            <img class="card-img-top img-fluid" [src]="service?.serviceImage?.fields?.file?.url | safeUrl">
            <div class="card-block">
              <h4 class="card-title">{{service?.serviceTitle}}</h4>
              <ul class="list-group list-group-flush">
                <li class="list-group-item bg-brand-black"><i class="fa fa-wrench mr-2" aria-hidden="true"></i>Cras justo odio</li>
                <li class="list-group-item bg-brand-black"><i class="fa fa-wrench mr-2" aria-hidden="true"></i>Dapibus ac facilisis in</li>
                <li class="list-group-item bg-brand-black"><i class="fa fa-wrench mr-2" aria-hidden="true"></i>Vestibulum at eros</li>
              </ul>
            </div>
            <div class="card-footer">
              <a href="#" class="btn btn-brand-red">Learn More</a>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</section>
...
1

1 Answers

0
votes

It sounds like the Contentful promise is resolving outside of Angular's zone.

You can ensure the the observable's methods are run inside the zone by injecting NgZone into your service:

import { NgZone } from '@angular/core';

constructor(private zone: NgZone) {
  this.client = contentful.createClient({
    space: SPACE_ID,
    accessToken: API_KEY
  });
}

And by calling using the injected zone's run when calling the subject's methods:

getEntriesByContentType(contentType, selectParam) {

  const subject = new Subject();

  this.client.getEntries({
    'content_type': contentType,
    'select': selectParam
  })
  .then(
    data => {
      this.zone.run(() => {
        subject.next(data);
        subject.complete();
      });
    },
    err => {
      this.zone.run(() => {
        subject.error(err);
        subject.complete();
      });
    }
  );

  return subject.asObservable();
}