Showing posts with label nativescript. Show all posts
Showing posts with label nativescript. Show all posts

Friday, January 5, 2018

When JSON has URLs but you need data (or how to chain API calls and merging the result)

The Problem

Let's say you have an API (ie. http://api.example.org/stuffs) that returns an array of stuff. But the API dev who worked on it, the bastard that he is, decided that certain fields, like say price is a hyperlink. So it looks like:

[{
   "id": "exde01"
   "name": "Stuff1",
   "type": "just stuff",
   "weight": "3.22",
   "price": "http://api.example.org/price/exde01"
},
{
   "id": "exde02"
   "name": "Stuff2",
   "type": "just stuff",
   "weight": "3.25",
   "price": "http://api.example.org/price/exde02"
}
.....
]

Ok. Now when you do an API call to the price url you'd get something like:

{
   "inlu_price": 69.96
   "exlu_tax": 49.99,
   "price": 123.25
}

So now the problem is how do we loop over the first JSON result with stuff and calling each price URL and then appending back the result. Essentially, we want the end result to look like this:

[{
   "id": "exde01"
   "name": "Stuff1",
   "type": "just stuff",
   "weight": "3.22",
   "price": {
     "inlu_price": 69.96
     "exlu_tax": 49.99,
     "price": 123.25
   }
},
{
   "id": "exde02"
   "name": "Stuff2",
   "type": "just stuff",
   "weight": "3.25",
   "price": {
     "inlu_price": 69.96
     "exlu_tax": 49.99,
     "price": 123.25
   }
}
.....
]

The Solution

My solution is in TypeScript and if it wasn't for reactive extensions (or streams) I'd probably have hanged myself. 

The first step is to get the main JSON. So I wrote a service for it.

    getAllStuff(): Observable {
        const url = "http://api.example.org/stuffs";

        return this.http.get(url}).map((response) => response.json());
    }

Now for the tricky part.

   this.restService.getAllStuff().switchMap((stuffs) => {
      return Observable.forJoin(stuffs.map((s) => {
         return this.http.get(s.price).map((res) => {
            s.price = res;
            return s;
         }
      }
   })
   .subscribe((r) => {
      console.log(JSON.stringify(r));
   });

This looks hard but it isn't really once you understand how it "flows".

First the getAllStuff() call will return the first set of results (technically, an Observable array) which we pass in into the forJoin() function.

What forJoin() does it take an array of API calls (as Observables) and spits out a single "merged" result. That's where the stuffs.map() does. But take note this is a two step process.

If you just focus on just the stuffs.map() . You can access stuff JSON array and drill down to the s.price, ie. ['http://api.example.org/price/exde01', 'http://api.example.org/price/exde02'].

This is why then we do a return with the this.http.get() function to produce that array forJoin() needs. ForJoin() then resolves these calls. After resolving, the result is passed back up to the switchMap() function which we can then subscribe to get the transformed JSON.

There we get our desired JSON result albeit with a bit of processing.

Now, if there's a 2nd URL in your main json, then you'll just need another switchMap(). You can chain multiple switchMaps().

References:

  • http://blog.danieleghidoli.it/2016/10/22/http-rxjs-observables-angular/
  • https://www.learnrxjs.io/operators/transformation/switchmap.html
  • https://stackoverflow.com/questions/38517552/angular-2-chained-http-get-requests-with-iterable-array

Monday, October 16, 2017

Doing listviews with empty lists and pullrefresh in Nativescript-ng

This was a pain in the butt to do right and I had to do a "bit" of trial and error to get this right.

All I wanted do was show a "centered" bold, large text on the screen when a list is empty and the actual list view when there's data on the list with both having support for pull to refresh action.

Googling this well probably lead you to Telerik's doc on the RadListView for pullToRefresh which I call shenanigans on because I couldn't make it to work. So we move to next idea which is Brad Martin's pullToRefresh nativescript plugin.

I did get Martin's nativescript plugin to work on my first attempt sans a bug that was a bit odd. I'm not even sure if the bug can be replicated on another machine. Anyhow, my first attempt's code looked like this:

 <PullToRefresh (refresh)="refreshList($event)" row="1">  
     <GridLayout horizontalAlignment="center" verticalAlignment="center"   
           rows="*" columns="*" *ngIf="emptyList()">  
       <Label horizontalAlignment="center" verticalAlignment="center" row="1" column="1" class="h2"   
           text="No Orders Available."></Label>  
     </GridLayout>   
     <ListView [items]="ordersList"   
          (itemTap)="onOrderTap($event)" class="list-group">  
       <ng-template let-order="item" let-odd="odd" let-even="even">  
         <GridLayout [class.odd]="odd" [class.even]="even" columns="auto, *" rows="auto">  
           <Label col="0" class="iconbkg" text=""></Label>  
           <avatar [avatardata]="order.avatar_prop"></avatar>  
           <StackLayout col="1" class="formMessage">  
             <GridLayout columns="auto, 100, *" rows="auto">  
               <Label class="h3 text-left" col="0" [text]="'Order #' + order.number"></Label>  
               <Label class="h3 text-right" col="1" [text]="order.date_placed | date:'MMMM dd yyyy'"></Label>  
             </GridLayout>                            
             <Label class="body" [text]="'City: ' + order.shipping_address.line4 + ', ' + order.shipping_address.state"></Label>  
             <Label class="body" [text]="order.lines.length + ' Item(s)'"></Label>  
           </StackLayout>  
         </GridLayout>  
       </ng-template>  
     </ListView>  
   </PullToRefresh>  


If you refer to the inner Gridlayout with the *ngIf element. The idea was to show this gridLayout when the list is empty. Don't bother with the ListView component because it will not show anything if [items] is empty.

The problem was when I do a pull to refresh, it was causing a crash in the android emulator saying that it was referencing a null view of some sort.

I eventually got it to do want I wanted. I did two things:

1. Move the empty list gridLayout into its own pullToRefresh container. Like so:

<PullToRefresh (refresh)="refreshList($event)" row="1" col="1" *ngIf="emptyList()">  
     <GridLayout horizontalAlignment="center" verticalAlignment="center" rows="*" columns="*">  
       <Label horizontalAlignment="center" verticalAlignment="center" row="1" column="1" class="h2"   
           text="No Orders Available."></Label>  
     </GridLayout>      
   </PullToRefresh>  

   <PullToRefresh (refresh)="refreshList($event)" row="1">  
     <ListView [items]="ordersList"   
          (itemTap)="onOrderTap($event)" class="list-group">
.....


2. I removed the ChangeDetectionStrategy in the @component declaration of the view class.

 With these two changes, the error no longer happened and it works perfectly.

Wednesday, August 30, 2017

Quick Letter Avatars for List in NativeScript

Letter Avatars are these elements often associated with Gmail - especially on it's Android client. So, being a programmer lacking in design skills, I decided to copy them for a Nativescript app I'm working on that had lists.

My approach to this was just to use a Label element and CSS. It's hacky but it works and in my world if it works it's not stupid.

So, we start on the component template:

 <GridLayout rows="*">  
  <ListView [items]="ordersList" (itemTap)="onOrderTap($event)" class="list-group">  
   <ng-template let-order="item" let-odd="odd" let-even="even">  
     <GridLayout [class.odd]="odd" [class.even]="even" columns="auto, *, auto">  
       <Label class="lavatar" text="VP" style="background-color: goldenrod; color: white;"></Label>  
       <StackLayout col="1" class="item-text">  
         <Label [text]="'#' + order.number"></Label>  
         <Label [text]="'Order by ' + order.guest_email"></Label>  
         <Label [text]="order.date_placed | date:'MMMM dd yyyy'"></Label>  
       </StackLayout>  
     </GridLayout>  
   </ng-template>  
  </ListView>  
 </GridLayout>   

The important thing here is the Label with the class "lavatar" which as the following rules:

.lavatar{
    text-align: center;
    font-size: 20;
    font-weight: bold;
    padding: 18;
    border-radius: 40;
    border-color: salmon;
    border-width: 0.5;
    width: 64px;
    height: 64px;
}

The border-radius is what's actually rounds the element.

Unfortunately this rule only works on "known" dimensions - width and height. For a more dynamic solution, I think you'll have to keep looking.

Settings the colors is based on the fact that CSS rules have the "cascade rule" so the final style rule applied is what's on the element. That's why the Label has a style attribute despite knowing it's in bad taste.

 Oh well.

Friday, August 11, 2017

NativeScript and that "status 200 url null" error

This error confused me while developing with NativeScript trying to do a rest api call. Without digging thru the error object, you only get: "Response with status: 200  for URL: null" for the error text. Not exactly useful.

I then had to run "tns debug android" and dig thru the error object by doing adding a "json()" method call to it. I then got a more verbose error message. Something like:

"Error: java.net.ConnectException: failed to connect to localhost/127.0.0.1 (port 8000): connect failed: ECONNREFUSED (Connection refused)"

The chrome devtools also was cryptic only saying that the rest api call "Stalled".

Ok. So I then visited StackOverflow looking for a solution. No dice. Until, I realized that I'm doing development using an android emulator which is basically a virtual machine. And being a virtual machine calling 'localhost' or 127.0.0.1 is calling itself. The address is loopback address of the vm NOT my machine. No wonder the rest api call was failing. Fortunately, the android emulator networking is setup to work with localhost development.

The solution is to just change the rest api call from 'http://localhost/api' to http://10.0.2.2/api'.

Now it works.

For the curious, my rest api server is a Django Rest Framework app running locally.