diff --git a/packages/overmind-website/examples/guide/goingfunctional/calloperator.ts b/packages/overmind-website/examples/guide/goingfunctional/calloperator.ts new file mode 100644 index 00000000..1dea6ed0 --- /dev/null +++ b/packages/overmind-website/examples/guide/goingfunctional/calloperator.ts @@ -0,0 +1,102 @@ +const javascript = { + react: [ + { + fileName: 'components/MyComponent.jsx', + target: 'jsx', + code: ` +import React from 'react' +import { connect } from '../overmind' + +const MyComponent = ({ overmind }) => ( + +) + +export default connect(MyComponent) + `, + }, + ], + vue: [ + { + fileName: 'components/MyComponent.vue (template)', + target: 'markup', + code: ` + + `, + }, + { + fileName: 'components/MyComponent.vue (script)', + code: ` +import { connect } from '../overmind' + +export default connect({}) + `, + }, + ], +} + +const typescript = { + react: [ + { + fileName: 'components/MyComponent.tsx', + code: ` +import * as React from 'react' +import { connect, Connect } from '../overmind' + +type Props = Connect + +const MyComponent: React.FunctionComponent = ({ overmind }) => ( + +) + +export default connect(MyComponent) + `, + }, + ], + vue: [ + { + fileName: 'components/MyComponent.vue (template)', + target: 'markup', + code: ` + + `, + }, + { + fileName: 'components/MyComponent.vue (script)', + code: ` +import { connect } from '../overmind' + +export default connect({}) + `, + }, + ], + angular: [ + { + fileName: 'components/grabdata.component.ts', + code: ` +import { Component,Input } from '@angular/core'; +import { connect } from '../overmind' + +@Component({ + selector: 'grab-data', + template: \` + + \` +}) +@connect() +export class GrabData {} + `, + }, + ], +} + +export default (ts, view) => (ts ? typescript[view] : javascript[view]) diff --git a/packages/overmind-website/examples/guide/goingfunctional/clean.ts b/packages/overmind-website/examples/guide/goingfunctional/clean.ts index d4e25b7b..ffd91899 100644 --- a/packages/overmind-website/examples/guide/goingfunctional/clean.ts +++ b/packages/overmind-website/examples/guide/goingfunctional/clean.ts @@ -1,23 +1,11 @@ export default (ts) => ts ? [ - { - fileName: 'overmind/operators.ts', - code: ` -import { Operator, map, filter } from 'overmind' - -const getEventTargetValue: Operator = - map(({ value }) => value.currentTarget.value) - -const lengthGreaterThan: (length: number) => Operator = - (length) => filter(({ value }) => value.length > length) - - `, - }, { fileName: 'overmind/actions.ts', code: ` import { Operator, pipe, debounce, action } from 'overmind' +import { getEventTargetValue, lengthGreaterThan } from './operators' export const search: Operator = pipe( getEventTargetValue, @@ -31,18 +19,21 @@ export const search: Operator = pipe( ) `, }, - ] - : [ { - fileName: 'overmind/operators.js', + fileName: 'overmind/operators.ts', code: ` -import { map, filter } from 'overmind' +import { Operator, map, filter } from 'overmind' -export const getEventTargetValue = map(({ value }) => value.currentTarget.value) +const getEventTargetValue: Operator = + map(({ value }) => value.currentTarget.value) -export const lengthGreaterThan = (length) => filter(({ value }) => value.length > length) +const lengthGreaterThan: (length: number) => Operator = + (length) => filter(({ value }) => value.length > length) + `, }, + ] + : [ { fileName: 'overmind/actions.js', code: ` @@ -61,4 +52,14 @@ export const search = pipe( ) `, }, + { + fileName: 'overmind/operators.js', + code: ` +import { map, filter } from 'overmind' + +export const getEventTargetValue = map(({ value }) => value.currentTarget.value) + +export const lengthGreaterThan = (length) => filter(({ value }) => value.length > length) + `, + }, ] diff --git a/packages/overmind-website/examples/guide/goingfunctional/factory.ts b/packages/overmind-website/examples/guide/goingfunctional/factory.ts new file mode 100644 index 00000000..8b5fe104 --- /dev/null +++ b/packages/overmind-website/examples/guide/goingfunctional/factory.ts @@ -0,0 +1,24 @@ +export default (ts) => + ts + ? [ + { + fileName: 'overmind/operators.ts', + code: ` +import { Operator, filter } from 'overmind' + +const lengthGreaterThan: (length: number) => Operator = + (length) => filter(({ value }) => value.length > length) + + `, + }, + ] + : [ + { + fileName: 'overmind/operators.js', + code: ` +import { map, filter } from 'overmind' + +export const lengthGreaterThan = (length) => filter(({ value }) => value.length > length) + `, + }, + ] diff --git a/packages/overmind-website/examples/guide/typescript/action.ts b/packages/overmind-website/examples/guide/typescript/action.ts new file mode 100644 index 00000000..3cf80b2f --- /dev/null +++ b/packages/overmind-website/examples/guide/typescript/action.ts @@ -0,0 +1,15 @@ +export default () => [ + { + code: ` +import { Action } from 'overmind' + +export const noArgAction: Action = ({ value }) => { + value // this becomes "void" +} + +export const argAction: Action = ({ value }) => { + value // this becomes "string" +} + `, + }, +] diff --git a/packages/overmind-website/examples/guide/goingfunctional/operatorinfer.ts b/packages/overmind-website/examples/guide/typescript/operatorinfer.ts similarity index 100% rename from packages/overmind-website/examples/guide/goingfunctional/operatorinfer.ts rename to packages/overmind-website/examples/guide/typescript/operatorinfer.ts diff --git a/packages/overmind-website/examples/guide/typescript/operatorinfer_solution.ts b/packages/overmind-website/examples/guide/typescript/operatorinfer_solution.ts new file mode 100644 index 00000000..5d58b0ef --- /dev/null +++ b/packages/overmind-website/examples/guide/typescript/operatorinfer_solution.ts @@ -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 = pipe( + doSomeStateChange(), + action(({ value: input, state }) => { + state.input = input + }) +) + `, + }, +] diff --git a/packages/overmind-website/examples/guide/goingfunctional/operatorinputsandoutputs.ts b/packages/overmind-website/examples/guide/typescript/operatorinputsandoutputs.ts similarity index 100% rename from packages/overmind-website/examples/guide/goingfunctional/operatorinputsandoutputs.ts rename to packages/overmind-website/examples/guide/typescript/operatorinputsandoutputs.ts diff --git a/packages/overmind-website/examples/guide/goingfunctional/operatorpartial.ts b/packages/overmind-website/examples/guide/typescript/operatorpartial.ts similarity index 67% rename from packages/overmind-website/examples/guide/goingfunctional/operatorpartial.ts rename to packages/overmind-website/examples/guide/typescript/operatorpartial.ts index a45ae3b4..2e0598e3 100644 --- a/packages/overmind-website/examples/guide/goingfunctional/operatorpartial.ts +++ b/packages/overmind-website/examples/guide/typescript/operatorpartial.ts @@ -4,7 +4,7 @@ export default () => [ code: ` import { Operator, filter } from 'overmind' -export const filterAwesomeUser: Operator<{ isAwesome: boolean }> = +export const filterAwesome: Operator<{ isAwesome: boolean }> = filter(({ value: somethingAwesome }) => somethingAwesome.isAwesome) `, }, @@ -12,11 +12,13 @@ export const filterAwesomeUser: Operator<{ isAwesome: boolean }> = fileName: 'overmind/actions.ts', code: ` import { Operator, pipe, action } from 'overmind' -import { filterAwesomeUser } from './operators' +import { filterAwesome } from './operators' import { User } from './state' export const clickedUser: Operator = pipe( - filterAwesomeUser, + // We get an error here, because this operator explicitly + // outputs the type { isAwesome: boolean } + filterAwesome, action(({ value: user, state }) => { state.awesomeUsersClickedCount++ }) diff --git a/packages/overmind-website/examples/guide/typescript/operatorpartial_solution.ts b/packages/overmind-website/examples/guide/typescript/operatorpartial_solution.ts new file mode 100644 index 00000000..49875961 --- /dev/null +++ b/packages/overmind-website/examples/guide/typescript/operatorpartial_solution.ts @@ -0,0 +1,26 @@ +export default () => [ + { + fileName: 'overmind/operators.ts', + code: ` +import { Operator, filter } from 'overmind' + +export const filterAwesome: () => Operator<{ isAwesome: boolean }, T> = + () => filter(({ value: somethingAwesome }) => somethingAwesome.isAwesome) + `, + }, + { + fileName: 'overmind/actions.ts', + code: ` +import { Operator, pipe, action } from 'overmind' +import { filterAwesome } from './operators' +import { User } from './state' + +export const clickedUser: Operator = pipe( + filterAwesome(), + action(({ value: user, state }) => { + state.awesomeUsersClickedCount++ + }) +) + `, + }, +] diff --git a/packages/overmind-website/examples/guide/goingfunctional/wrongoperator.ts b/packages/overmind-website/examples/guide/typescript/wrongoperator.ts similarity index 100% rename from packages/overmind-website/examples/guide/goingfunctional/wrongoperator.ts rename to packages/overmind-website/examples/guide/typescript/wrongoperator.ts diff --git a/packages/overmind-website/guides/intermediate/03_typescript.md b/packages/overmind-website/guides/intermediate/03_typescript.md index c697cc65..7ca9ca9b 100644 --- a/packages/overmind-website/guides/intermediate/03_typescript.md +++ b/packages/overmind-website/guides/intermediate/03_typescript.md @@ -2,7 +2,7 @@ Overmind is written in Typescript and it is written with a focus on your keeping as little time as possible helping Typescript understand what your app is all about. Typescript will spend a lot more time helping you. There are actually two approaches to typing in Overmind. -## 1. Declare module +## Declare module The most straight forward way to type your application is to use the **declare module** approach. This will work for most applications, but might make you feel uncomfortable as a harcore Typescripter. The reason is that we are overriding an internal type, meaning that you can only have one instance of Overmind running inside your application. @@ -16,9 +16,75 @@ Now you can import any type directly from Overmind and it will understand the co h(Example, { name: "guide/typescript/declare_imports.ts" }) ``` -## 2. Explicit typing +## Explicit typing You can also explicitly type your application. This gives more flexibility. ```marksy h(Example, { name: "guide/typescript/explicit.ts" }) -``` \ No newline at end of file +``` + +## Actions + +The action type takes either no arguments or a single argument. If you give no arguments to the action it will be typed as not expecting an argument at all. When you do type with an argument that is the type of the **value** on the context. This value is populated when you call the action on the Overmind instance. + +```marksy +h(Example, { name: "guide/typescript/action.ts" }) +``` + + +## Operators + +Operators is like the **Action** type, it can take an optional input, but it can also have an output. By default the output of an operator is the same as the input. Most operators has its output the same as the input, meaning the incoming values just passes through. + +```marksy +h(Example, { name: "guide/typescript/operatorinputsandoutputs" }) +``` + +Now what is important to understand is that the **Operator** is used with all operators in Overmind, cause all of them is about "passing a value to the next operator". The arguments you give has to match the specific operator though. So for example if you type an **action** operator with a different output than the input: + +```marksy +h(Example, { name: "guide/typescript/wrongoperator" }) +``` + +Typescript yells at you, because this operator just passes the value straight through. + +Typically you do not think about this and Typescript rather yells at you when the value you are passing through your operators is not matching up. + +### Caveats +There are two **limitations** to the operators type system which we are still trying to figure out: + +#### 1. Partial input type + +For example: + +```marksy +h(Example, { name: "guide/typescript/operatorpartial" }) +``` + +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. + +This can be handled by making the operator a factory instead: + +```marksy +h(Example, { name: "guide/typescript/operatorpartial_solution" }) +``` + +#### 2. Infer input + +You might create an operator that does not care about its input. For example: + +```marksy +h(Example, { name: "guide/typescript/operatorinfer" }) +``` + +Composing **doSomeStateChange** into the **pipe** gives an error, cause this operator expects a **void** type. There is no other way to make this work than inlining the **doSomeStateChange** operator into the pipe in question, which infers the correct input. + +You could use the same trick here though: + +```marksy +h(Example, { name: "guide/typescript/operatorinfer_solution" }) +``` + + +#### 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 :-) \ No newline at end of file diff --git a/packages/overmind-website/guides/intermediate/04_goingfunctional.md b/packages/overmind-website/guides/intermediate/04_goingfunctional.md index e4806631..93649f60 100644 --- a/packages/overmind-website/guides/intermediate/04_goingfunctional.md +++ b/packages/overmind-website/guides/intermediate/04_goingfunctional.md @@ -1,6 +1,6 @@ # Going functional -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. +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 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. @@ -18,9 +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 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 +## Calling an operator To get going with functional code you can simply convert any existing action by using the **action** operator. @@ -28,64 +26,34 @@ 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. Even though we used an operator you can still call this as normal from a component: - -```marksy -h(Example, { name: "guide/goingfunctional/callactionoperator" }) -``` - -## Piping - -To compose the different operators together you typically use **pipe**. You can also compose pipes into pipes, it is just an operator like the rest. - -```marksy -h(Example, { name: "guide/goingfunctional/pipe" }) -``` - -There are several operators available and you can quite easily create new operators from scratch. They er built with the [op-op spec](https://github.com/christianalfoni/op-op-spec). A specification designed specifically to lower the threshold of moving into the functional world. - -## Typescript - -All operators have the same type. - -`TOperator` - -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. +This makes your action a composable piece to be used with other operators. Even though we converted our action into an operator it can still be called as normal from a component: ```marksy -h(Example, { name: "guide/goingfunctional/operatorinputsandoutputs" }) +h(Example, { name: "guide/goingfunctional/calloperator" }) ``` -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. +But not only the **action** operator can be attached as a callable action on your Overmind instance, **any** operator can be used. That means you will have some operators that is only used for composition and some that are attached to the Overmind instance as an action to be called, typically from a component. +Here we see the **parallel** operator being used as a callable action to run two action operators in parallel: ```marksy -h(Example, { name: "guide/goingfunctional/wrongoperator" }) +h(Example, { name: "guide/goingfunctional/callactionoperator" }) ``` -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 :-) - -### Caveats -There are two **limitations** to the operators type system which we are still trying to figure out: +## Piping -#### 1. Partial input type -For example: +To compose the different operators together you typically use **pipe**. You can also compose pipes into pipes, it is just an operator like the rest. ```marksy -h(Example, { name: "guide/goingfunctional/operatorpartial" }) +h(Example, { name: "guide/goingfunctional/pipe" }) ``` -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. +There are several operators available and you can quite easily create new operators from scratch. They er built with the [op-op spec](https://github.com/christianalfoni/op-op-spec). A specification designed specifically to move state management solutions into a functional world. -#### 2. Infer input +## Factories -You might create an operator that does not care about its input. For example: +A familiar concept in functional programming is the use of factories. A factory is a function that creates an operator. This can be used to send *options* to an operator. A typical example of this is the **lengthGreaterThan** operator we built previously. ```marksy -h(Example, { name: "guide/goingfunctional/operatorinfer" }) -``` - -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 :-) \ No newline at end of file +h(Example, { name: "guide/goingfunctional/factory" }) +``` \ No newline at end of file