Raúl Jiménez

elecash@gmail.com

@elecash

Using Elm's Json Decoders in Angular

about me

Byte

Default

What's
Elm?

Definition

Elm is a programming language for declaratively creating web browser-based graphical user interfaces. Elm is purely functional, and is developed with emphasis on usability, performance, and robustness. It advertises "no runtime exceptions in practice".

Source: wikipedia

NO

RUNTIME

EXCEPTIONS

Features

  • Purely functional
    No OOP here
  • Elm Public Library
    Can detect changes in your dependencies
  • Immutability
    Enforced by the compiler
  • Statically typed
    With type inference like TypeScript

Elm's

Decoders

Why?

  • Web apps load a lot endpoints
  • How can we protect against those changes?
  • Front-end doesn't know if the endpoint changed
  • Web apps uses a lot of third party APIs

For example

We load this data

[
    {
        "title": "Beers",
        "count": 12,
        "tags": [ "beer", "Żywiec", "Okocim", "Tyskie" ],
        "available": true
    },
    {
        "title": "Nutella",
        "count": 1,
        "tags": [ "nutella", "chocolate", "cream", "gluttony" ],
        "available": true
    }
]

Later in your code...

We read the tags in the HTML

<ul>
    <li *ngFor="let item of (shoppingList$ | async).items">
        {{ item.title }}
        <p *ngIf="item.tags.length">
            <span *ngFor="let tag of sortedTags(item)">{{ tag }}, </span>
        </p>
    </li>
</ul>

Or TypeScript code

sortedTags(item: ShoppingItem) {
    return item.tags.sort();
}

The API changes...

We don't get noticed or a third-party lib publish a fix instead of a breaking change

[
    {
        "title": "Beers",
        "count": 12,
      	"meta": {
            "tags": [ "beer", "Żywiec", "Okocim", "Tyskie" ],          
        },
        "available": true
    },
    {
        "title": "Nutella",
        "count": 1,
        "meta": {
            "tags": [ "nutella", "chocolate", "cream", "gluttony" ],          
        },
        "available": true
    }
]

Result

We have a runtime exception in our template

<ul>
    <li *ngFor="let item of (shoppingList$ | async).items">
        {{ item.title }}
        <p *ngIf="item.tags.length">
            <span *ngFor="let tag of sortedTags(item)">{{ tag }}, </span>
        </p>
    </li>
</ul>

We have a runtime exception in TS code

sortedTags(item: ShoppingItem) {
    return item.tags.sort();
}

It could be worse...

If they change "tags" to an array of Objects

[
  {
    "title": "Beers",
    "count": 12,
    "tags": [
      { "name": "beer"}, { "name": "Żywiec"}, { "name": "Okocim"}, { "name": "Tyskie"}
    ],
    "available": true
  },
  {
    "title": "Nutella",
    "count": 1,
    "tags": [
      { "name": "nutella" }, { "name": "chocolate" }, { "name": "cream" }, { "name": "gluttony" }
    ],
    "available": true
  }
]

Result

If we're not sorting the list it will display

[object Object]

<ul>
    <li *ngFor="let item of (shoppingList$ | async).items">
        {{ item.title }}
        <p *ngIf="item.tags.length">
            <span *ngFor="let tag of item.tags">{{ tag }}, </span>
        </p>
    </li>
</ul>

The
Elm's Decoders

Definition

A decoder is a function that can take a piece of JSON and decode it into an Elm value, with a type that matches a type that Elm knows about.

Decoders in Elm

Taking as example our JSON file:

import Json.Decode as Decoder

shoppingItemDecoder : Decoder.Decoder ShoppingItem
shoppingItemDecoder =
    Decoder.map4 
    (\title count tags available -> ShoppingItem title count tags available)
        (Decoder.field "title" Decoder.string)
        (Decoder.field "count" Decoder.int)
        (Decoder.field "tags" (Decoder.list Decoder.string))
        (Decoder.field "available" Decoder.bool)

shoppingListDecoder : Decoder.Decoder ShoppingList
shoppingListDecoder = 
    Decoder.list shoppingItemDecoder

We can transform our JSON file to an Elm value with the "shoppingListDecoder" function

Decoders in Angular

Introducing ts.data.json

  • TypeScript library
  • Works perfectly with NGRX
  • Framework agnostic
  • There are others but this is ours

Features

  • String
  • Number
  • Boolean
  • Objects and strict Objects
  • Arrays and Dictionaries
  • Optional values
  • Failovers
  • and more...

decoder.ts

Create a data decoder

export const ShoppingItemDecoder = JsonDecoder.object<ShoppingItem>({
    title: JsonDecoder.string,
    count: JsonDecoder.number,
    tags: JsonDecoder.array<string>(JsonDecoder.string, 'ShoppingItemTags[]'),
    available: JsonDecoder.boolean
}, 'ShoppingItemDecoder');

export const ShoppingListDecoder = JsonDecoder.array<ShoppingItem>(
    ShoppingItemDecoder, 'ShoppingItemDecoder[]'
);

We can nest decoders to create complex objects or reuse decoders

app.component.ts

Execute action to load the data from the view

ngOnInit() {
  this.subscriptions.push(
    this.actions$.pipe(
      filter(action => 
             action.type === ShoppingListActions.LOAD_SHOPPING_LIST_FAILURE)
    ).subscribe(
      action => console.log(action)
    )
  );

  this.store.dispatch(loadShoppingList());
}

If we want to react programmatically to the actions we can subscribe to them

service.ts

Load the data from the service

@Injectable()
export class ShoppingListService {
    constructor(public http: HttpClient) {}

    getShoppingList() {
        return this.http.get<ShoppingItem[]>('assets/mocks/list.json').pipe(
            concatMap(p => fromPromise(ShoppingListDecoder
                .decodePromise(p)
                .catch(e => {
                    throw new Error(e);
                })
            ))
        );
    }
}

And decode the result...

or catch the errors

effects.ts

Process the response in the effects

loadShoppingList$ = createEffect(() => this.actions$.pipe(
    ofType(ShoppingListActions.LOAD_SHOPPING_LIST),
    mergeMap(() => this.shoppingListService.getShoppingList()
        .pipe(
            map(items => ({
              type: ShoppingListActions.LOAD_SHOPPING_LIST_SUCCESS,
              payload: items
            })),
            catchError(error => of({
              type: ShoppingListActions.LOAD_SHOPPING_LIST_FAILURE,
              payload: error.message
            }))
        )
    )
));

And return a success...

or an error

Demo using NGRX

Print the data in the template

<span *ngIf="(shoppingList$ | async).isLoading">loading...</span>

<ul>
    <li *ngFor="let item of (shoppingList$ | async).items">
        {{ item.title }}
        <p *ngIf="item.tags.length">
            <span *ngFor="let tag of sortedTags(item)">{{ tag }}, </span>
        </p>
    </li>
</ul>

<div *ngIf="(shoppingList$ | async).error">
    {{ (shoppingList$ | async).error }}
</div>

Demo

Summary

Summary

  • Decoders protects our UI against API changes
  • Some API changes may be hard to detect
  • We can nest decoders to reuse code
  • It's easy to integrate into an NGRX workflow
  • We can react to decoding errors easily

Resources

Resources

GRACIAS