Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/joanllenas/ngx-remotedata

💥 Synchronize data fetching and UI state
https://github.com/joanllenas/ngx-remotedata

angular elm ngrx pipes remotedata ui ui-antipattern

Last synced: 15 days ago
JSON representation

💥 Synchronize data fetching and UI state

Awesome Lists containing this project

README

        

# RemoteData

**Slaying a UI Antipattern with Angular.**

[![Build Status](https://travis-ci.org/joanllenas/ngx-remotedata.svg?branch=master)](https://travis-ci.org/joanllenas/ngx-remotedata)
[![npm version](https://badge.fury.io/js/ngx-remotedata.svg)](https://www.npmjs.com/package/ngx-remotedata)
[![npm downloads](https://img.shields.io/npm/dm/ngx-remotedata)](https://www.npmjs.com/package/ngx-remotedata)

Library inspired by [Kris Jenkins](https://twitter.com/krisajenkins) blog post about [How Elm slays a UI antipattern](http://blog.jenkster.com/2016/06/how-elm-slays-a-ui-antipattern.html), which mixes pretty well with [another article](http://scotthurff.com/posts/why-your-user-interface-is-awkward-youre-ignoring-the-ui-stack) written by [Scott Hurff](https://twitter.com/scotthurff) about what he calls the _UI Stack_.

![](./.github/ui-stack-framed.jpg)

## Table Of Contents

- [What we are trying to solve](#trying-to-solving)
- [The traditional approach](#traditional-approach)
- [The RemoteData approach](#remotedata-approach)
- [Installation](#installation)
- [Basic Usage](#basic-usage)
- [Examples](#examples)
- [Demo 👀](#examples-demo)
- [Basic](#examples-basic)
- [Services](#examples-services)
- [Ngrx](#examples-ngrx)
- [Api 📚](#api)
- [Pipes 📚](#pipes)

## What we are trying to solve

We are making an API request and want to display different things based on the request's status.

### The traditional approach

```ts
export interface SunriseSunset {
isInProgress: boolean;
error: string;
data: {
sunrise: string;
sunset: string;
};
}
```

Let us see what each property means:

- `isInProgress`: It is `true` while the data is being fetched.
- `error`: It is either `null` (no errors) or any `string` (there are errors).
- `data`: Either `null` (no data) or the result payload (there is data).

There are a few problems with this approach, the main one being that it is possible to create **invalid states** such as:

```json
{
"isInProgress": true,
"error": "Fatal error",
"data": {
"sunrise": "I am good data.",
"sunset": "I am good data too!"
}
}
```

Our html template will have to use complex `*ngIf` statements to make sure we are displaying the correct information.

### The RemoteData approach â„¢

Instead of using a complex data structures we use a single data type to express all possible request states:

```ts
type RemoteData = NotAsked | InProgress | Failure | Success;
```

This approach **makes it impossible to create invalid states**.

## Installation

`npm install --save ngx-remotedata`

## Basic Usage

```ts
// app.module.ts

import { RemoteDataModule } from 'ngx-remotedata';

@NgModule({
imports: [
// (...)
RemoteDataModule
]
})
```

```ts
// app.component.ts

import {
RemoteData,
inProgress,
notAsked,
success,
failure,
} from 'ngx-remotedata';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent {
remoteData: RemoteData = notAsked();

setNotAsked() {
this.remoteData = notAsked();
}

setInProgress() {
this.remoteData = inProgress('In progress...');
}

setSuccess() {
this.remoteData = success('Success!');
}

setFailure() {
this.remoteData = failure('Wrong!');
}
}
```

```html


  • Not Asked

  • InProgress

  • Success

  • Failure


Not Asked


InProgress...



{{ remoteData | successValue }}



{{ remoteData | failureValue }}


```

## Examples

#### Demo 👀

- [Stackblitz Ngrx demo](https://stackblitz.com/edit/ngx-remotedata-demo?file=src%2Fapp%2Fmeow%2Freducer.ts)

#### Source code

- [The basics](src/app/examples/basics)

- [Plain old services](src/app/examples/pos)

- [Ngrx](src/app/examples/ngrx) _(includes store rehydration from `localStorage`)_

## Api

### RemoteData 📚

`RemoteData`

`RemoteData` is used to annotate your request variables. It wraps all possible request states into one single union type. Use the parameters to specify:

- `T`: The success value type.
- `E`: The error value type (`string` by default).

- Type guard function: `isRemoteData = (value: unknown): value is RemoteData`.

### NotAsked 📚

- Constructor function: `notAsked(): RemoteData`.
- Type guard function: `isNotAsked(value: unknown): value is NotAsked`.

When a `RemoteData` is a `NotAsked` instance, it means that the request hasn't been made yet.

```ts
type User = { email: string };
const myRemoteData: RemoteData = notAsked();

// (...)

if (isNotAsked(myRemoteData)) {
// Here myRemoteData is narrowed to NotAsked
}
```

### InProgress 📚

- Constructor function: `inProgress(value?: T): RemoteData`.
- Type guard function: `isInProgress(value: unknown): value is InProgress`.

When a `RemoteData` is an `InProgress` instance, it means that the request has been made, but it hasn't returned any data yet.

The `InProgress` instance can contain a value of the same `T` type as `Success`. Useful when you want to use the last `Success` value while the new data is being fetched.

```ts
type User = { email: string };
const myRemoteData: RemoteData = inProgress({ email: '[email protected]' });

// (...)

if (isInProgress(myRemoteData)) {
// Here myRemoteData is narrowed to InProgress
console.log(`I have some data: ${myRemoteData.value.email}`);
}
```

### Success 📚

- Constructor function: `success(value: T): RemoteData`.
- Type guard function: `isSuccess(value: unknown): value is Success`.

When a `RemoteData` is a `Success` instance, it means that the request has completed successfully and the new data (of type `T`) is available.

```ts
type User = { email: string };
const myRemoteData: RemoteData = success({ email: '[email protected]' });

// (...)

if (isSuccess(myRemoteData)) {
// Here myRemoteData is narrowed to Success
console.log(`I have some data: ${myRemoteData.value.email}`);
}
```

### Failure 📚

- Constructor function: `failure(err: E, val?: T): RemoteData`.
- Type guard function: `isFailure(value: unknown): value is Failure`.

When a `RemoteData` is a `Failure` instance, it means that the request has failed. You can get the error information (of type `E`) from the payload.

The `Failure` instance can contain a value of the same `T` type as `Success`. Useful when you want to use the last `Success` value while displaying the failure message.

```ts
type User = { email: string };
const myRemoteData: RemoteData = failure('Something went wrong.', {
email: '[email protected]',
});

// (...)

if (isFailure(myRemoteData)) {
// Here myRemoteData is narrowed to Failure
console.log(`This is the failure: ${myRemoteData.error}`);
console.log(`I have some data: ${myRemoteData.value.email}`);
}
```

The default type for errors is `string`, but you can also provide other types like `Error`:

```ts
type User = { email: string };
const myRemoteData: RemoteData = failure(
new Error('Something went wrong.')
);
```

## Unwrapping RemoteData values

### getOrElse 📚

```ts
getOrElse(rd: RemoteData, defaultValue: T): T;
```

`getOrElse` _unwraps_ and returns the value of `Success` instances or the `defaultValue` when it's any other `RemoteData` variant.

```ts
// Example
let myRemoteData = success('ok!');
console.log(getOrElse(myRemoteData, 'The default value')); // ok!

myRemoteData = failure('There has been an error');
console.log(getOrElse(myRemoteData, 'The default value')); // The default value
```

### fold 📚

```ts
fold(
onNotAsked: () => T,
onInProgress: (value: T | undefined) => T,
onFailure: (error: E, value: T | undefined) => T,
onSuccess: (value: T) => T,
rd: RemoteData
): T;
```

With `fold` you _unwrap_ the `RemoteData` value by providing a function for each of the type variants.

```ts
// Example
const rd = success('this is fine!');
const result = fold(
() => 'not asked',
(val) => 'in progress: ' + val,
(error, value) => `failure: ${error} ${value}`,
(value) => 'success: ' + value,
rd
);
console.log(result); // success: this is fine!
```

## Transforming RemoteData values

### map 📚

```ts
map(
fn: (a: A) => B,
rd: RemoteData

): RemoteData;
```

With `map` you provide a transformation function that is applied to a `RemoteData` only when it's a `Success` instance.

```ts
// Example
const scream = (s: string) => s.toUpperCase();
const hello = success('hello!');
const helloScreaming = map(scream, hello);
console.log(helloScreaming); // success('HELLO!')
```

### mapFailure 📚

```ts
mapFailure
(
fn: (e: E) => F,
rd: RemoteData

): RemoteData
;
```

With `mapFailure` you provide a transformation function that is applied to a `RemoteData` only when it's a `Failure` instance.

```ts
// Example
const scream = (s: string) => s.toUpperCase();
const error = failure('wrong!');
const wrongScreaming = mapFailure(scream, error);
console.log(wrongScreaming); // failure('WRONG!')
```

### chain 📚

```ts
chain
(
fn: (a: A) => RemoteData,
rd: RemoteData

): RemoteData;
```

With `chain` you can provide a transormation function that can change the returned `RemoteData` variant.

```ts
// Example
const checkAge = (n: number) =>
n >= 0 ? success(n) : failure(`${n} is an invalid age`);
let ageResult = chain(checkAge, success(25));
expect(ageResult).toEqual(success(25));
ageResult = chain(checkAge, success(-3));
expect(ageResult).toEqual(failure('-3 is an invalid age'));
```

## RxJs operators

### filterSuccess 📚

Specialized version of the rxjs `filter` operator for `RemoteData` values.
Emits only when source `Observable` is a `Success`, also narrows the emitted value to `Success`.

```ts
// Example
const myRemoteData = success(3);
of(myRemoteData)
.pipe(
filterSuccess(),
map((s) => s.value * 2)
)
.subscribe((n) => {
console.log(n); // 6
});
```

### filterFailure 📚

Specialized version of the rxjs `filter` operator for `RemoteData` values.
Emits only when source `Observable` is a `Failure`, also narrows the emitted value to `Failure`.

```ts
// Example
const myRemoteData = failure('wrong!');
of(myRemoteData)
.pipe(
filterFailure(),
map((f) => 'Error: ' + f.error)
)
.subscribe((err) => {
console.log(err); // 'Error: wrong!'
});
```

## Pipes

### isNotAsked 📚

`transform(rd: RemoteData): boolean;`

Returns `true` when `RemoteData` is a `NotAsked` instance.

### anyIsNotAsked 📚

```ts
transform(
rds$: Observable>[]
): boolean;
```

Returns `true` when any `RemoteData[]` items is a `NotAsked` instance.

### isInProgress 📚

`transform(rd: RemoteData): boolean;`

Returns `true` when `RemoteData` is an `InProgress` instance.

### anyIsInProgress 📚

```ts
transform(
rds$: Observable>[]
): boolean;
```

Returns `true` when any `RemoteData[]` item is an `InProgress` instance.

### isFailure 📚

`transform(rd: RemoteData): boolean;`

Returns `true` when `RemoteData` is a `Failure` instance.

### isSuccess 📚

`transform(rd: RemoteData): boolean;`

Returns `true` when `RemoteData` is a `Success` instance.

### hasValue 📚

`transform(rd: RemoteData): boolean;`

Returns `true` when `RemoteData` is a `Success` instance or is an `InProgress` or `Failure` instance with a value that is not `null` or `undefined`.

### successValue 📚

```ts
transform(
rd: RemoteData,
defaultValue?: T
): T | undefined;
```

Returns the `Success` payload (of type `T`) when the `RemoteData` is a `Success` instance, otherwise it returns the `defaultValue` when provided or `undefined` when not.

### inProgressValue 📚

```ts
transform(
rd: RemoteData,
defaultValue?: T | undefined
): T | undefined;
```

Returns the `InProgress` payload (of type `T`) when `RemoteData` is an `InProgress` instance, otherwise it returns the provided `defaultValue` or `undefined` when not.

### remoteDataValue 📚

`transform(rd: RemoteData): T | E | undefined;`

Returns the `InProgress`, `Failure` or `Success` payload (of type `T`) when `RemoteData` is an `InProgress`, `Failure` or `Success` instance. Returns `undefined` otherwise.

### failureError 📚

`transform(rd: RemoteData): E | undefined`

Returns the `Failure` error payload (of type `E`) when `RemoteData` is a `Failure` instance or `undefined` otherwise.

### failureValue 📚

`transform(rd: RemoteData): T | undefined`

Returns the `Failure` payload (of type `T`) when `RemoteData` is a `Failure` instance or `undefined` otherwise.