Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a Reference section to the docs #132

Merged
merged 22 commits into from
Jan 8, 2024
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:

strategy:
matrix:
node-version: [14.x]
node-version: [16.14.x]

env:
NPM_EMAIL: ''
Expand Down
4 changes: 2 additions & 2 deletions documentation/docs/guides/configurableApplications.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,11 @@ export class ApplicationGraphForTests extends ApplicationGraph {
To use the graph in the test, we'll use Obsidian's test kit to use the `ApplicationGraphForTests` instead of the `ApplicationGraph` whenever it's needed.

```ts
import {testKit} from 'react-obsidian';
import {mockGraphs} from 'react-obsidian';

describe('Test suite', () => {
beforeEach(() => {
testKit.mockGraphs({
mockGraphs({
// Instruct Obsidian to use the ApplicationGraphForTests instead of the ApplicationGraph
ApplicationGraph: ApplicationGraphForTests,
});
Expand Down
146 changes: 146 additions & 0 deletions documentation/docs/reference/mediatorObservable.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
---
sidebar_position: 2
title: 'MediatorObservable'
---

`MediatorObservable` **is a type of** `Observable` **that acts as an adapter between one or more source** `Observable`**s. It allows us to create a new observable stream based on the values of other observables.**

For example, we can create a `MediatorObservable` that observes an Observable of a certain type and emits a new value based on the value of the source observable.

```ts
const user = new Observable<User>();

const isLoggedIn = new MediatorObservable<boolean>();
isLoggedIn.add(user, (nextUser) => isLoggedIn.value = nextUser !== undefined);
```

* [Reference](#reference)
* [new MediatorObservable(initialValue?)](#new-observableinitialvalue)
* [addSource(source, onNext): this](#addsourcesource-onnext-this)
* [mapSource(source, mapNext): this](#mapsourcesource-mapnext-this)
* [addSources(sources, onNext): this](#addsourcessources-onnext-this)
* [mapSources(sources, mapNext): this](#mapsourcessources-mapnext-this)
* [async first&#60;T&#62;(): Promise&#60;T&#62;](#async-firstt-promiset)
* [Usage](#usage)
* [Observing multiple sources](#observing-multiple-sources)
* [Conditional rendering of a component](#conditional-rendering-of-a-component)
___
## Reference
### new MediatorObservable(initialValue?)
Creates a new `MediatorObservable` with an optional initial value.
#### Arguments
* `initialValue?` - The initial value of the `MediatorObservable`. Defaults to `undefined`.

#### Caveats
* It's possible to instantiate a `MediatorObservable` without an initial value, but it's not recommended, as its value will be `undefined` until it's set for the first time which can lead to unexpected behavior.
___

### addSource(source, onNext): this
Starts observing the given source and calls the `onNext` callback when the source emits a new value.

#### Arguments
* `source` - The source `Observable` to observe.
* `onNext` - The callback to be called when the source emits a new value. The `onNext` callback receives the new value of the source as an argument. __In order to update the value of the `MediatorObservable`, call the [value setter](observable#set-value) with the new value__.

#### Returns
`addSource` returns the `MediatorObservable` instance, so it can be chained with other methods.
___

### mapSource(source, mapNext): this
Starts observing the given source and calls the `onNext` callback when the source emits a new value.

#### Arguments
* `source` - The source `Observable` to observe.
* `mapNext` - The callback to be called when the source emits a new value. The `mapNext` callback receives the new value of the source as an argument and __must return the new value of the `MediatorObservable`__.

#### Returns
`mapSource` returns the `MediatorObservable` instance, so it can be chained with other methods.

---
### addSources(sources, onNext): this
Similar to [addSource](#addsourcesource-onnext-this), but accepts an array of sources instead of a single source. Use this method when the logic of the `onNext` callback is the same for all sources.

#### Arguments
* `sources` - An array of source `Observable`s to observe.
* `onNext` - The callback to be called when any of the sources emits a new value. The `onNext` callback receives the current values of all sources as arguments. In order to update the value of the `MediatorObservable`, call the [value setter](observable#set-value) with the new value.

#### Returns
`addSources` returns the `MediatorObservable` instance, so it can be chained with other methods.

---
### mapSources(sources, mapNext): this
Similar to [mapSource](#mapsourcesource-mapnext-this), but accepts an array of sources instead of a single source. Use this method when the logic of the `mapNext` callback is the same for all sources.

#### Arguments
* `sources` - An array of source `Observable`s to observe.
* `mapNext` - The callback to be called when any of the sources emits a new value. The `mapNext` callback receives the current values of all sources as arguments and should return the new value of the `MediatorObservable`.

#### Returns
`mapSources` returns the `MediatorObservable` instance, so it can be chained with other methods.

---
### async first&#60;T&#62;(): Promise&#60;T&#62;
See [Observable.first()](observable#async-firstt-promiset).

## Usage
### Observing multiple sources
Sometimes data is computed from the values of other observables. We can use `MediatorObservable` to create a new observable that observes other observables and emits a new value based on their values.

Let's walk through an example to see how this works. Say we're developing a live streaming app and want to change the quality of the stream based on the device's battery level and the network speed. We'll create two observables, `batteryLevel` and `networkSpeed`, and we'll merge their emissions in one new `MediatorObservable` called `streamQuality`. By doing so, `batteryLevel` and `networkSpeed` will become the sources of the new `MediatorObservable`.

```ts
enum NetworkSpeed { Poor = 150, Moderate = 550, Good = 2000 }
enum streamQuality { Low, Medium, High }

const batteryLevel = new Observable<number>();
const networkSpeed = new Observable<number>();

// Every time one of the sources emits a new value, the mediator will call the callback function
const streamQuality = new MediatorObservable<streamQuality>().mapSources(
[batteryLevel, networkSpeed],
(batteryLevel, networkSpeed) => {
if (batteryLevel < 15 || networkSpeed < NetworkSpeed.Poor) {
return streamQuality.Low;
}

if (networkSpeed < NetworkSpeed.Moderate) {
return StreamQuality.Medium;
}

return StreamQuality.High;
}
);
```

Now, every time the battery level or the network speed changes, the `streamQuality` observable will emit a new value based on the new values of the sources.

### Conditional rendering of a component
When optimizing React applications for performance, avoiding unnecessary renders is one of the most important things to do. One way to do this is to use `Observable` to conditionally render a component only when a certain condition is met. We can use `MediatorObservable` to create a new observable that observes another and emits a value only when a condition is met.

To illustrate this, let's create a `MediatorObservable` that observes a user's score in a game. The `MediatorObservable` will emit a new value when the user's score is greater than 10. We'll use this observable to conditionally render a component that displays a message when the user wins the game.

First, we'll declare the observables that we'll use:
```tsx
// An observable that tracks a user's score in a game
const gameScoreObservable = new Observable<number>(0);

const isWinnerObservable = new MediatorObservable<boolean>(false)
.addSource(gameScoreObservable, (score) => {
if (score > 10) {
gameState.value = true;
}
});
```

Now we can use the `isWinnerObservable` to conditionally render the game's status:
```tsx
const Game = () => {
const [isWinner] = useObserver(isWinnerObservable);

return (
<div>
{isWinner && <p>You won!</p>}
<GameBoard />
</div>
)
}
49 changes: 49 additions & 0 deletions documentation/docs/reference/model.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
sidebar_position: 5
title: 'Model'
---

`Model` **is an abstract utility class that provides an easy way to observe specific properties of an object.**

* [Reference](#reference)
* [use()](#use)
* [Usage](#usage)
* [Observing properties](#observing-properties)

## Reference
### use()
The `use` method is used to observe the properties of a model. It's intended to be used only in hooks.
#### Returns
An object containing all of the model's observable properties.

## Usage
### Observing properties
Since `Model` is an abstract class, you'll need to extend it to use it. Let's say you have an app state that looks like this:
```ts
class AppState {
public user = new Observable<User>();
public isLoggedIn = new Observable<boolean>();
}
```

You can use `Model` to observe the properties of `AppState` like this:

```ts
import { injectHook, Model } from 'react-obsidian';

// 1. Make AppState extend Model
class AppState extends Model {
public user = new Observable<User>(); // { firstName: string; lastName: string;}
public isLoggedIn = new Observable<boolean>();
}

// 2. `appState` is injected into the hook
const _useUserName = (appState: AppState) => {
// 3. Use `appState.use()` to observe the properties
const {user, isLoggedIn} = user.use();

return `${user.firstName} is ${isLoggedIn ? '' : 'not '}logged in`;
};

export const useUserName = injectHook(_useUserName, /* SomeGraph */);
```
65 changes: 65 additions & 0 deletions documentation/docs/reference/observable.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
sidebar_position: 1
title: 'Observable'
---

`Observable` **is a class that represents a stream of values. It is similar to** `Promise` **in that it is a container for a value that will be available in the future. However, unlike **`Promise`**, **`Observable`** can emit multiple values over time.**

```ts
const isLoggedIn = new Observable(false);
isLoggedIn.subscribe((nextValue: boolean) => {
if (nextValue) {
console.log('User is logged in');
} else {
console.log('User is logged out');
}
});
```

* [Reference](#reference)
* [new Observable(initialValue?)](#new-observableinitialvalue)
* [subscribe(onNext)](#subscribeonnext)
* [unsubscribe(onNext)](#unsubscribeonnext)
* [set value](#set-value)
* [get value](#get-value)
* [async first&#60;T&#62;(): Promise&#60;T&#62](#async-firstt-promiset)
* [Usage](#usage)
* [Conditional rendering of a component](#conditional-rendering-of-a-component)
___

### Reference
#### new Observable(initialValue?)
Creates a new `Observable` instance with an optional initial value.
#### Arguments
* `initialValue?` - The initial value of the `Observable`. Defaults to `undefined`.

#### Caveats
* It's possible to instantiate an `Observable` without an initial value, but it's not recommended, as its value will be `undefined` until it's set for the first time which can lead to unexpected behavior.
___
### subscribe(onNext)
The `subscribe` method is used to listen for changes to the `Observable`'s value. It returns a function that can be used to unsubscribe from the `Observable`.
#### Arguments
* `onNext` - A function that will be called whenever the `Observable`'s value changes. It receives the new value as an argument.
#### Returns
* `unsubscribe` - A function that can be used to unsubscribe from the `Observable`.
___
### unsubscribe(onNext)
The `unsubscribe` method is used to unsubscribe from the `Observable` a specific `onNext` callback.
#### Arguments
* `onNext` - The `onNext` callback to unsubscribe.
___
### set value
The `value` property is used to set the `Observable`'s value. Changing the value will trigger all subscribers and will trigger a rerender if the `Observable` is used in a Component or a Hook.
___
### get value
The `value` property is used to get the `Observable`'s current value.
___
### async first&#60;T&#62;(): Promise&#60;T&#62;
The `first` method is used to get the `Observable`'s first value. If the `Observable` has no value, it will wait for the first value to be set and return it.

#### Returns
* `Promise` - A `Promise` that resolves to the `Observable`'s current value if it has one, or waits for the first value to be set and resolves to it.

## Usage
### Conditional rendering of a component
See [Conditional rendering of a component](./mediatorObservable#conditional-rendering-of-a-component) for a detailed explanation of this example.
9 changes: 9 additions & 0 deletions documentation/docs/reference/testKit/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"label": "TestKit",
"position": 6,
"collapsed": true,
"link": {
"type": "generated-index",
"description": "Learn how to inject hooks, components and classes with Obsidian."
}
}
74 changes: 74 additions & 0 deletions documentation/docs/reference/testKit/mockGraphs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
sidebar_position: 1
title: 'mockGraphs'
---

`mockGraphs` **is a function that is used in tests to replace the implementation of graphs with a mock implementation.**

The `mockGraphs` function is meant to be used only in tests. It is especially useful when writing integration tests. Unlike in unit tests, where a single unit is tested in isolation, integration tests involve testing multiple **concrete** objects together. By doing so we can validate that the objects work together as expected. Sometimes, we may want to mock some of the dependencies instead of using concretions. For example, we wouldn't want to use a real database in our tests or send real HTTP requests. `mockGraphs` lets us replace certain objects with fakes or mocks.

* [Reference](#reference)
* [mockGraphs(graphNameToGraph)](#mockgraphsgraphnametograph)

## Reference
### mockGraphs(graphNameToGraph)
Replaces the implementation of the given graphs with mock implementations.
#### Arguments
* `graphNameToGraph` - An object mapping graph names to graphs. The graph names must be the same as the names of the graphs being mocked.

## Usage
### Mocking a graph
Lets say we have a graph that looks like this:
```ts
@Singleton() @Graph()
class AppGraph {
@Provides()
storage(): Storage {
return new Storage();
}
}
```

The Storage class is a simple class that persists data to local storage. We don't want to use the real Storage class in our tests as it would make our tests slow and unpredictable. Instead, we'll create a fake implementation of Storage that stores data in memory.

```ts
class FakeStorage extends Storage {
private data: Record<string, string> = {};

override getItem(key: string): string | undefined {
return this.data[key];
}

override setItem(key: string, value: string) {
this.data[key] = value;
}
}
```

Next, we'll create a graph that provides a fake implementation of Storage.

```ts
@Singleton() @Graph()
class AppGraphForIntegrationTests {
@Provides()
override storage(): Storage {
return new FakeStorage();
}
}
```

Finally, we'll mock the AppGraph in our tests by calling `mockGraphs` with an object mapping the name of the graph to the mocked graph.
```ts
import { Obsidian, mockGraphs } from 'react-obsidian';

describe('Mocking graphs', () => {
beforeEach(() => {
mockGraphs({ AppGraph: AppGraphForIntegrationTests });
});

it('should use the fake storage', () => {
const storage = Obsidian.obtain(AppGraph).storage();
expect(storage).toBeInstanceOf(FakeStorage);
});
});
```
Loading
Loading