Skip to content

Commit

Permalink
WIP: new docs (#174)
Browse files Browse the repository at this point in the history
* notes
* cleanup
* add easy testing
* add observe
* add deriveUpdates
* add server communication
* add create-react-app example
* add steal example
* add recompose docs
* add tic-tac-toe example
* update recompose docs
* update examples for 2.1.0
* advanced docs
  • Loading branch information
Christopher Baker committed Jul 20, 2018
1 parent 27d3991 commit 5813d60
Show file tree
Hide file tree
Showing 51 changed files with 29,396 additions and 37 deletions.
14 changes: 8 additions & 6 deletions connect/README.md
Expand Up @@ -13,7 +13,7 @@ When your state updates, your components automatically re-render, allowing you t
```js
import React, { Component } from "react";
import ReactDOM from "react-dom";
import ylem, { ObserveObject } from "ylem";
import { connect, ObserveObject } from "ylem";

class Store extends ObserveObject {
count = 0
Expand All @@ -29,7 +29,8 @@ class Store extends ObserveObject {

class MyComponent extends Component {
render() {
const { count, increment, decrement } = props;
const { count, increment, decrement } = this.props;

return (
<div>
<button onClick={decrement}>-</button>
Expand All @@ -40,7 +41,7 @@ class MyComponent extends Component {
}
}

const App = ylem(Store, MyComponent);
const App = connect(Store)(MyComponent);

ReactDOM.render(<App />, document.getElementById("root"));
```
Expand Down Expand Up @@ -78,8 +79,8 @@ npm install ylem
Once **ylem** is installed, modify `src/App.js` to match the example below _(copy and paste)_. Feel free to look over what is going on here, but we'll take each part in turn.

```js
import React from "react";
import "./App.css";
import React from "react";
import ylem, { ObserveObject } from "ylem";

var store = new ObserveObject({
Expand Down Expand Up @@ -170,7 +171,7 @@ const Counter = props => (
</div>
);

// Use ylem to connect your Store to your Component
// connect your Store to your Component
export default ylem(Store, Counter);
```

Expand All @@ -196,6 +197,7 @@ This `appstate` will be an observable object instance, representing our top leve
```js
// appstate.js
import { ObserveObject } from "ylem";

class AppState extends ObserveObject {
user = null

Expand All @@ -212,8 +214,8 @@ Authentication in a real app is much more complicated than what we show here. Th
Now, back in our `App.js` component, let's import the `appstate` instance and use the `user` property and `login` method to show how you can split your stores into independent entities, but still keep the reactive nature for your auto-rendering UI components.

```js
import React from "react";
import "./App.css";
import React from "react";
import ylem, { ObserveObject } from "ylem";
import appstate from "./appstate";

Expand Down
25 changes: 16 additions & 9 deletions connect/docs/advanced-topics.md
@@ -1,9 +1,16 @@
# Advanced Topics

- Easy Testing
- Using `createComponent()` to make components that accept a render prop
- Using `connect()`, `withViewModel()` as an Enhancer with the Recompose library
- Architecting with an MVVM architecture
- Using `observe()` to create **observer components** to get extra rendering efficiency
- Resolving name conflicts with the `mapProps` option in `ylem()` and `connect()`
- ???
## Advanced Topics

### [Easy Testing](./easy-testing.md)
With the separation of View and Store provided by ylem, testing follows the same pattern and becomes much easier: your views and stores can be tested in isolation without regard for each other.

### [Using `observe()` for Extra Rendering Efficiency](./observe.md)
Even when not connecting a store to your component, ylem can still be used to increase efficiency by observing sub-component renders.

### [Resolving Prop Name Conflicts with `deriveUpdates`](./derive-updates.md)
There are a number of reasons to rename props as they come into your store. Perhaps you want to provide a simple external api but need to move and modify those prior to use. Perhaps you want to modify the values provided by the user while still maintaining access to the original values. Either way, ylem's `deriveUpdates` can help.

### [Backend Services](./backend-services.md)
Inevitably, you will need to access backend services. This is exceedingly simple with ylem: any api that uses promises (including `fetch`) will seamlessly integrate. Use [can-realtime-rest-model](https://canjs.com/doc/can-realtime-rest-model.html) for more advanced models.

### [Use with the Recompose library](./recompose.md)
Like higher-order components? Seamlessly create enhancers for recompose using `withStore`, an alias of `connect`.
24 changes: 12 additions & 12 deletions connect/docs/api.md
@@ -1,17 +1,17 @@
# API
## API

- `ylem()`
- `ylem(Component)` -> returns an observer component (useful for making more efficient renders)
- `ylem(ObserveObject, Component)` -> return and enhanced component
- `ylem(ObserveObject, Component, opts)` -> return and enhanced component (options include the `mapProps` function)
- `ylem(ObserveObject)(Component)` -> if no component is provided it returns a function that accepts a component
- `ylem(ObserveObject, opts)(Component)` -> same as above but with options
- `ylem(Component)` -> returns an observer component (useful for making more efficient renders)
- `ylem(ObserveObject, Component)` -> return and enhanced component
- `ylem(ObserveObject, Component, opts)` -> same as above but with options (options include the `deriveUpdates` function)
- `ylem(ObserveObject)(Component)` -> if no component is provided it returns a function that accepts a component
- `ylem(ObserveObject, opts)(Component)` -> same as above but with options (options include the `deriveUpdates` function)
- `ObserverObject`
- `.listenTo`
- ObserveObject Decorators
- `@async`
- `@resolver`
- `.listenTo`
- ObserveObject Decorators
- `@getAsync`
- `@resolvedBy`
- `ObserveArray`
- `connect(ObservableClass)` alias `withViewModel`
- `createComponent(ObservableClass)`
- `connect(ObservableClass, opts)` alias `withStore`
- `createComponent(ObservableClass, opts)`
- `observer(Component)`
41 changes: 41 additions & 0 deletions connect/docs/backend-services.md
@@ -0,0 +1,41 @@
## Backend Services

Inevitably, you will need to communicate with a backend. Ylem makes this very easy.

As long as you have a promise, it can be easily incorporated with Ylem. The resolved value is cached and will be used until its observable dependencies (in this case, `search`) change.

```js
import { getAsync } from 'ylem/property-decorators';

export class ResultsStore extends ObserveObject {
search = ""

setSearch = (search) => {
this.search = search;
}

@getAsync
get results() {
return fetch(`https://swapi.co/api/people/?search=${this.search}`)
.then(response => response.json())
.then(response => response.results)
;
}
}

export const Results = ({ search, setSearch, results }) => (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />

{ results ? (
<div>
{ results.map(({ url, name }) => (
<div key={url}>{name}</div>
)) }
</div>
) : null }
</div>
);

export default connect(ResultsStore)(Results);
```
65 changes: 65 additions & 0 deletions connect/docs/derive-updates.md
@@ -0,0 +1,65 @@
## Resolving Prop Name Conflicts with `deriveUpdates`

Ylem provides a `deriveUpdates` option, allowing you to adjust props before they are passed into your Store.

Often, especially when dealing with form components, you want to allow the user to specify an incoming value and also track internal changes to that value. Further, it is preferable if the incoming prop is something simple like `title`, and for your own sanity that the same simple name of `title` be used internally for tracking changes. This presents a unique problem however, where these two uses of the same name will conflict.

Here we have a `Form` component, which would be rendered as `<Form title="Default Title" />`. How would you implement the reset here though, as the original values have been overwritten.

```js
export class FormStore extends ObserveObject {
title = ""

setTitle = (title) => {
this.title = title;
}

submit = () => {
return fetch(...);
}

reset = () => {
// how?
}
}

export const Form = ({ title, setTitle, reset }) => (
<form>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
</form>
);

export default connect(FormStore)(Form);
```

We *could* have the user pass in `defaultTitle` (and `defaultName` and `defaultX`...), then add all the logic to connect defaults with current. *Or* you could just rename the incoming `title` to `defaultTitle`, use it to initialize `title`, and re-use it later to reset the values.

```js
export class FormStore extends ObserveObject {
title = this.defaultTitle

setTitle = (title) => {
this.title = title;
}

submit = () => {
return fetch(...);
}

reset = () => {
this.title = this.defaultTitle;
}
}

export const Form = ({ title, setTitle, reset }) => (
<form>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
</form>
);

export default connect(FormStore, {
deriveUpdates: ({ title }) => ({ defaultTitle: title}),
})(Form);
```

_Note: Though we are using this with `connect`, it is also available with `ylem` and `createComponent`.
107 changes: 107 additions & 0 deletions connect/docs/easy-testing.md
@@ -0,0 +1,107 @@
## Easy Testing

One of the benefits of separating your View from your CounterStore is ease of testing: your view need only test that it shows the correct information and that it calls the correct handler functions; your store need only test that it provides the correct information.

_Note: These tests are written using [mocha](https://mochajs.org/) and the assertions are using the [chai assertion library](http://www.chaijs.com/api/bdd/). This process is testing system agnostic however, and can be ported to whatever test runner you are using._

### The App

For example, lets say you have this app. We will need two kinds of tests.

```js
export class CounterStore extends ObserveObject {
count = 0

increment = () => {
this.count++;
}
}

export const Counter = ({ count, increment }) => (
<div>
<span id="count">{count}</span>
<button onClick={increment}>+</button>
</div>
);

export default ylem(CounterStore, Counter);
```

### Testing the Store

The first tests are for the Store, `store.test.js`. We want to verify the initial value of `count`, that `increment` increases that count, and that `increment` is bound to the instance, so React can call it properly.

```js
import { expect } from 'chai';

import { CounterStore } from './app.js';

describe('CounterStore', () => {
describe('count property', () => {
it('is initialized at 0', () => {
const store = new CounterStore();

expect(store).to.have.property('count', 0);
});
});

describe('increment method', () => {
it('increases the count property', () => {
const store = new CounterStore();
store.increment();

expect(store).to.have.property('count', 1);
});

it('is bound to the instance', () => {
const store = new CounterStore();

const increment = store.increment;
increment();

expect(store).to.have.property('count', 1);
});
});
});
```

### Testing the View

Lastly, we need tests for the View, `view.test.js`. In this case, we need only verify that the count is shown as expected, and that clicking the button calls the `increment` function.

```js
import { expect } from 'chai';
import TestRenderer from 'react-test-renderer';

import { Counter } from './app.js';

describe('Counter', () => {
describe('count property', () => {
it('is displayed as expected', () => {
const renderer = TestRenderer.create(
<Counter count={29} />
);

// note: children is an array
const count = renderer.root.findByProps({ id: 'count' });
expect(count.children).to.include('29');
});
});

describe('increment property', () => {
it('is called when the button is clicked', () => {
let called = false;
const renderer = TestRenderer.create(
<Counter increment={() => called = true} />
);

const buttons = renderer.root.findAllByType('button');
expect(called).to.equal(false);
buttons[1].props.onClick();
expect(called).to.equal(true);
});
});
});
```

By separating the component into its Store and View, we can separate our tests the same way. This makes them simpler, and makes it much easier to find problems. Further, if you were to re-use this View with multiple Stores, all you need to do is test that all the stores pass this same suite.
6 changes: 0 additions & 6 deletions connect/docs/examples.md

This file was deleted.

2 changes: 1 addition & 1 deletion connect/docs/getting-started-steal.md
Expand Up @@ -35,7 +35,7 @@ You will also need an `index.html` to load in the browser.
<title>ylem Demo</title>
</head>
<body>
<div id="app"></div>
<div id="root"></div>
<script src="node_modules/steal/steal.js"></script>
</body>
</html>
Expand Down

0 comments on commit 5813d60

Please sign in to comment.