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

Using Elm's Json Decoders in Angular

By Raúl Jiménez

Using Elm's Json Decoders in Angular

Angular Elements slides for my talk at ng-poland 2019

  • 1,752