Skip to content

Commit

Permalink
docs(website): improved some docs and added view helper
Browse files Browse the repository at this point in the history
  • Loading branch information
christianalfoni committed Jan 5, 2019
1 parent 75e3f2b commit fbfa173
Show file tree
Hide file tree
Showing 13 changed files with 394 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,9 @@ import { connect, Connect } from '../overmind'
type Props = {
item: { title: string }
}
} & Connect
const Item: React.SFC<Connect & Props> = ({ item }) => (
const Item: React.SFC<Props> = ({ item }) => (
<li>{item.title}</li>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
const javascript = {
react: [
{
fileName: 'overmind/actions.js',
code: `
import { parallel, action } from 'overmind'
export const grabData = parallel(
action(async ({ state, api }) => {
state.posts = await api.getPosts()
}),
action(async ({ state, api }) => {
state.users = await api.getUsers()
})
)
`,
},
{
fileName: 'components/MyComponent.jsx',
target: 'jsx',
code: `
import React from 'react'
import { connect } from '../overmind'
const MyComponent = ({ overmind }) => (
<button onClick={overmind.actions.grabData}>
Grab some data
</button>
)
export default connect(MyComponent)
`,
},
],
vue: [
{
fileName: 'overmind/actions.js',
code: `
import { parallel, action } from 'overmind'
export const grabData = parallel(
action(async ({ state, api }) => {
state.posts = await api.getPosts()
}),
action(async ({ state, api }) => {
state.users = await api.getUsers()
})
)
`,
},
{
fileName: 'components/MyComponent.vue (template)',
target: 'markup',
code: `
<button on:click="overmind.actions.grabData()">
Grab some data
</button>
`,
},
{
fileName: 'components/MyComponent.vue (script)',
code: `
import { connect } from '../overmind'
export default connect({})
`,
},
],
}

const typescript = {
react: [
{
fileName: 'overmind/actions.js',
code: `
import { Operator, parallel, action } from 'overmind'
export const grabData: Operator = parallel(
action(async ({ state, api }) => {
state.posts = await api.getPosts()
}),
action(async ({ state, api }) => {
state.users = await api.getUsers()
})
)
`,
},
{
fileName: 'components/MyComponent.tsx',
code: `
import * as React from 'react'
import { connect, Connect } from '../overmind'
type Props = Connect
const MyComponent: React.SFC<Props> = ({ overmind }) => (
<button onClick={overmind.actions.grabData}>
Grab some data
</button>
)
export default connect(MyComponent)
`,
},
],
vue: [
{
fileName: 'overmind/actions.ts',
code: `
import { Operator, parallel, action } from 'overmind'
export const grabData: Operator = parallel(
action(async ({ state, api }) => {
state.posts = await api.getPosts()
}),
action(async ({ state, api }) => {
state.users = await api.getUsers()
})
)
`,
},
{
fileName: 'components/MyComponent.vue (template)',
target: 'markup',
code: `
<button on:click="overmind.actions.grabData()">
Grab some data
</button>
`,
},
{
fileName: 'components/MyComponent.vue (script)',
code: `
import { connect } from '../overmind'
export default connect({})
`,
},
],
angular: [
{
fileName: 'overmind/actions.js',
code: `
import { Operator, parallel, action } from 'overmind'
export const grabData: Operator = parallel(
action(async ({ state, api }) => {
state.posts = await api.getPosts()
}),
action(async ({ state, api }) => {
state.users = await api.getUsers()
})
)
`,
},
{
fileName: 'components/grabdata.component.ts',
code: `
import { Component,Input } from '@angular/core';
import { connect } from '../overmind'
@Component({
selector: 'grab-data',
template: \`
<button (click)="overmind.actions.grabData()">
Grab some data
</button>
\`
})
@connect()
export class GrabData {}
`,
},
],
}

export default (ts, view) => (ts ? typescript[view] : javascript[view])
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export default () => [
{
fileName: 'overmind/operators.ts',
code: `
import { Operator, action } from 'overmind'
export const doSomeStateChange: Operator =
action(({ state }) => {
state.foo = 'bar'
})
`,
},
{
fileName: 'overmind/actions.ts',
code: `
import { Operator, pipe, action } from 'overmind'
import { doSomeStateChange } from './operators'
export const setInput: Operator<string> = pipe(
doSomeStateChange,
action(({ value: input, state }) => {
state.input = input
})
)
`,
},
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export default () => [
{
code: `
import { Operator, action, filter, map } from 'overmind'
// You do not need to define any types, which means it defaults
// its input and output to "void"
export const changeSomeState: Operator = action(({ state }) => {
state.foo = 'bar'
})
// The second type argument is not set, but will default to "User"
// The output is the same as the input
export const filterAwesomeUser: Operator<User> =
filter(({ value: user }) => user.isAwesome)
// "map" produces a new output so we define that as the second
// type argument
export const getEventTargetValue: Operator<Event, string> =
map(({ value: event }) => event.currentTarget.value)
`,
},
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export default () => [
{
fileName: 'overmind/operators.ts',
code: `
import { Operator, filter } from 'overmind'
export const filterAwesomeUser: Operator<{ isAwesome: boolean }> =
filter(({ value: somethingAwesome }) => somethingAwesome.isAwesome)
`,
},
{
fileName: 'overmind/actions.ts',
code: `
import { Operator, pipe, action } from 'overmind'
import { filterAwesomeUser } from './operators'
import { User } from './state'
export const clickedUser: Operator<User> = pipe(
filterAwesomeUser,
action(({ value: user, state }) => {
state.awesomeUsersClickedCount++
})
)
`,
},
]
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ import Post from './Post'
const Posts: React.SFC<Connect> = ({ overmind }) => (
<ul>
{overmind.state.postsList.map(post =>
<Item key={post.id} post={post} />
<Post key={post.id} post={post} />
)}
</ul>
)
Expand All @@ -123,7 +123,7 @@ export default connect(Posts)
code: `
import { Component,Input } from '@angular/core';
import { connect } from '../overmind'
import { Item } from '../overmind/state'
import { Post } from '../overmind/state'
@Component({
selector: 'app-post',
Expand Down
50 changes: 36 additions & 14 deletions packages/overmind-website/guides/intermediate/04_goingfunctional.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Going functional

You get very building your application with straight forward imperative actions. This is typically how we learn programming and is arguably close to how we think about the world. But this approach lacks a good structured way to compose multiple smaller pieces together. Reusing existing logic in multiple contexts. As the complexity of your application increases you will find benefits doing some of your logic, or maybe all your logic, in a functional style.
You get very far building your application with straight forward imperative actions. This is typically how we learn programming and is arguably close to how we think about the world. But this approach lacks a good structured way to compose multiple smaller pieces together. Reusing existing logic in multiple contexts. As the complexity of your application increases you will find benefits doing some of your logic, or maybe all your logic, in a functional style.

Let us look at a concrete example of how messy an imperative approach would be compared to a functional approach.

```marksy
h(Example, { name: "guide/goingfunctional/messy" })
```

What we see here is an action trying to express doing a search. We only want to search when the length of the query is more than 2 and we only want to trigger the search when the user has not pressed any keys for 200 milliseconds.
What we see here is an action trying to express doing a search. We only want to search when the length of the query is more than 2 and we only want to trigger the search when the user has not changed the query for 200 milliseconds.

If we were to do this in a functional style it would look more like this:

Expand All @@ -18,7 +18,7 @@ h(Example, { name: "guide/goingfunctional/clean" })

Now we have created a couple of custom operators that we can reuse in other compositions. In addition we have made our code declarative. Instead of showing implementation details we rather "tell the story of the code".

The great thing about the operator API is that you can use it to any extent you want, even drop it if the complexity of your app does not reach a level where it makes sense.
The great thing about the operator API is that you can think of it is "upgrading" from an action, cause you can use actions as an operator as well. You can use operators to any extent you want, even drop it completely if the complexity of your app does not reach a level where it makes sense to use them.

## Converting actions to functional actions

Expand All @@ -28,7 +28,11 @@ To get going with functional code you can simply convert any existing action by
h(Example, { name: "guide/goingfunctional/actionoperator" })
```

This makes your action a composable piece to be used with other operators. But actually **all** operators can be called as an action, not only the action operator. When you attach an operator to your actions configuration, you can call them from components.
This makes your action a composable piece to be used with other operators. Even though we used an operator you can still call this as normal from a component:

```marksy
h(Example, { name: "guide/goingfunctional/callactionoperator" })
```

## Piping

Expand All @@ -46,24 +50,42 @@ All operators have the same type.

`TOperator<Config, Input, Output>`

That means all operators has an input and an output. For most of the operators the output is the same as input, though with others, like **map**, it produces a new output. When you use the consumable type, either directly from Overmind or with explicit typing, there are some defaults.
That means all operators has an input and an output. For most of the operators the output is the same as input, though with others, like **map**, it produces a new output.

```marksy
h(Example, { name: "guide/goingfunctional/operatorinputsandoutputs" })
```

Now what is important to understand is that the Operator type will yell at you if you use it incorrectly with an actual operator. For example if you define a different output than input for an action operator. That is because an action operator is typed to pass its input as output.

```marksy
h(Example, { name: "guide/goingfunctional/wrongoperator" })
```

Just like the **Action** type, the **Operator** type does not need any arguments. That means it expects no value to be passed in.
You will also get yelled at by Typescript if you compose together operators that does not match outputs with inputs. But yeah, that is why we use it :-)

`Operator`
### Caveats
There are two **limitations** to the operators type system which we are still trying to figure out:

If you do define an *input*, that also becomes the *output*.
#### 1. Partial input type

`Operator<string>`
For example:

Or you can of course define both.
```marksy
h(Example, { name: "guide/goingfunctional/operatorpartial" })
```

`Operator<string, number>`
There is no way to express in Typescript that you should uphold this partial typing of **filterAwesomeUser** and still pass the inferred input, **User**, as the output. This will give a typing error.

Now what is important to understand is that the Operator type will yell at you if you use it incorrectly with an actual operator. For example if you define a different output than input for an action operator. That is because an action operator is typed to pass its input as output.
#### 2. Infer input

You might create an operator that does not care about its input. For example:

```marksy
h(Example, { name: "guide/goingfunctional/wrongoperator" })
h(Example, { name: "guide/goingfunctional/operatorinfer" })
```

You will also get yelled at by Typescript if you compose together operators that does not match outputs with inputs. But yeah, that is why we use it :-)
If you were to compose this action into a **pipe** you would get an error, cause this operator now expects a **void** type. You would have to inline the **doSomeStateChange** operator into the pipe in question to infer the correct input.

#### Summary
Typescript is not functional, it is object oriented and it is very difficult to make it work with this kind of APIs. We have high hopes though cause Typescript is evolving rapidly and Microsoft is dedicated to make this an awesome type system for JavaScript. If you got mad Typescript skills, please contact us to iterate on the type system and make this stuff work :-)
Loading

0 comments on commit fbfa173

Please sign in to comment.