1

My application is generated with latest @angular/cli.

Angular v.: 4.0.3, RxJS v.: 5.1, Zone.js: 0.8.4, Typescript: 2.2.2

I need to integrate CartoDB into my app. I created a service that will request and store data. Here it is:

// ========== carto.service.ts ==========
import {Injectable} from '@angular/core';

declare var cartodb: any;

@Injectable()
export class CartoService {
  private _sql: any;
  private _data: any;

  constructor() {
    this._data = this.getDataFromCarto('SELECT * FROM table');
  }

  get data() {
    return this._data;
  }

  getDataFromCarto(query: string): any {
    this._sql = new cartodb.SQL({user: username});

    return this._sql.execute(query).done((data) => {
      return data.rows;
    }).error((errors) => {
      console.error(`Was not able to get data: ${errors}`);
    });
  }
}

I did not found type definition for cartodb.js and was not able to write it myself. And so I just connected it as JS file and declared a new variable that matches carto`s global variable.

My component:

// ========== widget.component.ts ===========
import {Component, OnInit} from '@angular/core';
import {CartoService} from 'carto.service';

@Component({
  selector: 'app-widget',
  templateUrl: 'widget.component.html',
  styleUrls: ['widget.component.scss']
})
export class WidgetTableComponent implements OnInit {
  public data: any;

  constructor(private carto: CartoService) {
    super();
    this.data = this.carto.data;
  }

  ngOnInit() {
  }
}

And as a final step I want to show this data in template:

// ========= widget.component.html ==========
<tbody>
  <tr *ngFor="let entry of data">
    <th scope="row">{{entry.year}}</th>
  </tr>
</tbody>

While data is not resolved I receive an object with callback. *ngFor should not iterate though it while data is not resolved successfully.

Solutions I tried:


  1. RxJS and Observable.subscribe() do not work. Angular thinks that asynchronous actions are only browser events, http requests and timers. Carto`s request is a Jquery callback I guess and not considered asynchronous.

  1. *ngIf doesn`t work also. It hides only when variable is equal to null. In my case 'data' equals to:

{"_callbacks":{"done":{"tail":{},"next":{"next":{}}},"error":{"tail":{},"next":{"next":{}}}}}

When response is received template will not be rendered again.


  1. Elvis operator (? to mark variable as optional) can be used only with params. If I used it like this it will cause template render error:

<tr *ngFor="let entry of data?">


  1. Async Pipe:

<tr *ngFor="let entry of (data | async)">

Will cause this error:

ERROR Error: Uncaught (in promise): Error: InvalidPipeArgument: '[object Object]' for pipe 'AsyncPipe'


  1. ngOnChanges doesn`t see my variable changed. It was not present in the output of:

    ngOnChanges(changes) { console.log('All changes: ', changes); }


Did anyone encounter issue like this before? I would appreciate any help. Thanks in advance.

oliverfrost21
  • 259
  • 1
  • 3
  • 9

1 Answers1

1

Option 1 (with rxjs)

You can make your service return an Observable and use Subject to push a data into this Observable after sql execution completes:

@Injectable()
export class CartoService {
  private dataSbj = new Subject<any>();
  private dataObs = this.dataSbj.asObservable();

  getDataFromCarto(): Observable<any> {
    let sql = new cartodb.SQL({user: username});

    sql.execute('SELECT * FROM table')
      .done(data => this.dataSbj.next(data.rows))
      .error((errors) => {
         console.error(`Was not able to get data: ${errors}`);
      });
    return this.dataObs;
  }
}

Component:

@Component({
  selector: 'app-widget',
  templateUrl: 'widget.component.html',
  styleUrls: ['widget.component.scss']
})
export class WidgetTableComponent {
  data: Observable<any>;

  constructor(private carto: CartoService) {
    super();
    this.data = carto.getDataFromCarto();
  }
}

Template:

<tbody>
  <tr *ngFor="let entry of data | async">
    <th scope="row">{{entry.year}}</th>
  </tr>
</tbody>

Option 2 (with callback)

You can change your service to call some callback function instead of returning a data:

@Injectable()
export class CartoService {    
  getDataFromCarto(callback: (rows: any) => void): void {
    let sql = new cartodb.SQL({user: username});

    sql.execute('SELECT * FROM table')
      .done(data => callback(data.rows))
      .error((errors) => {
         console.error(`Was not able to get data: ${errors}`);
      });
   }
}

In the component you can call your service and pass a callback function which sets a data when it's ready:

@Component({
  selector: 'app-widget',
  templateUrl: 'widget.component.html',
  styleUrls: ['widget.component.scss']
})
export class WidgetTableComponent {
  data: any;

  constructor(private carto: CartoService) {
    super();
    carto.getDataFromCarto(rows => {this.data = rows});
  }
}

Now your *ngFor should work as expected. The data property will be empty till the service fills it with some rows in the callback.

<tbody>
  <tr *ngFor="let entry of data">
    <th scope="row">{{entry.year}}</th>
  </tr>
</tbody>
hiper2d
  • 2,814
  • 1
  • 18
  • 14