diff --git a/packages/overmind-website/examples/guide/connectingcomponents/object_2.ts b/packages/overmind-website/examples/guide/connectingcomponents/object_2.ts index 5094b6b9..6532924f 100644 --- a/packages/overmind-website/examples/guide/connectingcomponents/object_2.ts +++ b/packages/overmind-website/examples/guide/connectingcomponents/object_2.ts @@ -87,9 +87,9 @@ import { connect, Connect } from '../overmind' type Props = { item: { title: string } -} +} & Connect -const Item: React.SFC = ({ item }) => ( +const Item: React.SFC = ({ item }) => (
  • {item.title}
  • ) diff --git a/packages/overmind-website/examples/guide/goingfunctional/callactionoperator.ts b/packages/overmind-website/examples/guide/goingfunctional/callactionoperator.ts new file mode 100644 index 00000000..4a432507 --- /dev/null +++ b/packages/overmind-website/examples/guide/goingfunctional/callactionoperator.ts @@ -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 }) => ( + +) + +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: ` + + `, + }, + { + 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 = ({ overmind }) => ( + +) + +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: ` + + `, + }, + { + 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: \` + + \` +}) +@connect() +export class GrabData {} + `, + }, + ], +} + +export default (ts, view) => (ts ? typescript[view] : javascript[view]) diff --git a/packages/overmind-website/examples/guide/goingfunctional/operatorinfer.ts b/packages/overmind-website/examples/guide/goingfunctional/operatorinfer.ts new file mode 100644 index 00000000..ca4d9834 --- /dev/null +++ b/packages/overmind-website/examples/guide/goingfunctional/operatorinfer.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/goingfunctional/operatorinputsandoutputs.ts new file mode 100644 index 00000000..93aafac0 --- /dev/null +++ b/packages/overmind-website/examples/guide/goingfunctional/operatorinputsandoutputs.ts @@ -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 = + filter(({ value: user }) => user.isAwesome) + +// "map" produces a new output so we define that as the second +// type argument +export const getEventTargetValue: Operator = + map(({ value: event }) => event.currentTarget.value) + `, + }, +] diff --git a/packages/overmind-website/examples/guide/goingfunctional/operatorpartial.ts b/packages/overmind-website/examples/guide/goingfunctional/operatorpartial.ts new file mode 100644 index 00000000..a45ae3b4 --- /dev/null +++ b/packages/overmind-website/examples/guide/goingfunctional/operatorpartial.ts @@ -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 = pipe( + filterAwesomeUser, + action(({ value: user, state }) => { + state.awesomeUsersClickedCount++ + }) +) + `, + }, +] diff --git a/packages/overmind-website/examples/guide/managinglists/render2.ts b/packages/overmind-website/examples/guide/managinglists/render2.ts index b77e0199..39c97687 100644 --- a/packages/overmind-website/examples/guide/managinglists/render2.ts +++ b/packages/overmind-website/examples/guide/managinglists/render2.ts @@ -107,7 +107,7 @@ import Post from './Post' const Posts: React.SFC = ({ overmind }) => (
      {overmind.state.postsList.map(post => - + )}
    ) @@ -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', diff --git a/packages/overmind-website/guides/intermediate/04_goingfunctional.md b/packages/overmind-website/guides/intermediate/04_goingfunctional.md index f8d4d72c..e4806631 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 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. @@ -8,7 +8,7 @@ Let us look at a concrete example of how messy an imperative approach would be c 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: @@ -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 @@ -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 @@ -46,24 +50,42 @@ 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. 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` +For example: -Or you can of course define both. +```marksy +h(Example, { name: "guide/goingfunctional/operatorpartial" }) +``` -`Operator` +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 :-) \ No newline at end of file +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 diff --git a/packages/overmind-website/src/components/FrontPage/styles.ts b/packages/overmind-website/src/components/FrontPage/styles.ts index 735e4059..bee7f807 100644 --- a/packages/overmind-website/src/components/FrontPage/styles.ts +++ b/packages/overmind-website/src/components/FrontPage/styles.ts @@ -19,8 +19,32 @@ export const wrapper = css` export const summary = css` margin-top: 50px; + position: relative; font-size: var(--font-size-4); text-align: center; + font-style: italic; + line-height: 32px; + :before { + content: '“'; + color: var(--color-gray-1); + font-size: 48px; + font-weight: bold; + } + :after { + position: absolute; + color: var(--color-gray-1); + content: '”'; + font-size: 48px; + font-weight: bold; + } + @media (max-width: 700px) { + font-size: var(--font-size-2); + line-height: 26px; + :before, + :after { + font-size: 32px; + } + } ` export const container = css` diff --git a/packages/overmind-website/src/components/ViewSelector/index.tsx b/packages/overmind-website/src/components/ViewSelector/index.tsx index 5aee1f1a..a95c00c1 100644 --- a/packages/overmind-website/src/components/ViewSelector/index.tsx +++ b/packages/overmind-website/src/components/ViewSelector/index.tsx @@ -3,7 +3,6 @@ import { useOvermind } from '../../overmind' import * as ReactImage from '../../images/react.png' import * as VueImage from '../../images/vue.png' import * as AngularImage from '../../images/angular.png' -import * as OvermindImage from '../../images/overmind.png' import * as TsImage from '../../images/ts.png' import * as TsImageGrayscale from '../../images/ts-grayscale.png' import Icon from '../Icon' @@ -48,7 +47,10 @@ const ViewSelector: SFC = () => { return (
    -
    +
    {state.typescript ? ( ) : ( @@ -62,7 +64,7 @@ const ViewSelector: SFC = () => {
    {options[state.theme]} @@ -83,6 +85,22 @@ const ViewSelector: SFC = () => { ))}
    ) : null} + {state.showViewHelp ? ( +
    +
    +
    + You can select a different view layer and toggle Typescript up + here +
    +
    + Got it! +
    +
    +
    + ) : null}
    ) } diff --git a/packages/overmind-website/src/components/ViewSelector/styles.ts b/packages/overmind-website/src/components/ViewSelector/styles.ts index 0ef08b15..3b107844 100644 --- a/packages/overmind-website/src/components/ViewSelector/styles.ts +++ b/packages/overmind-website/src/components/ViewSelector/styles.ts @@ -91,3 +91,48 @@ export const option = css` margin-right: 5px; } ` + +export const viewHelpWrapper = css` + position: absolute; + top: calc(100% + 10px); + width: 300px; + right: 0; +` + +export const viewHelpArrow = css` + position: relative; + border-radius: 5px; + background-color: var(--color-dark-1); + box-shadow: 0px 3px 20px -10px rgba(0, 0, 0, 0.75); + :after { + bottom: 100%; + left: 50%; + border: solid transparent; + content: ' '; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border-color: rgba(136, 183, 213, 0); + border-bottom-color: var(--color-dark-1); + border-width: 10px; + margin-left: -10px; + } +` + +export const viewHelpText = css` + font-size: var(--font-size-3); + color: var(--color-white-1); + padding: var(--padding-4); +` + +export const viewHelpButton = css` + padding: var(--padding-3) 0; + cursor: pointer; + text-align: center; + color: var(--color-dark-1); + background-color: var(--color-white-2); + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + font-weight: bold; +` diff --git a/packages/overmind-website/src/overmind/actions.ts b/packages/overmind-website/src/overmind/actions.ts index a174756d..631f2330 100644 --- a/packages/overmind-website/src/overmind/actions.ts +++ b/packages/overmind-website/src/overmind/actions.ts @@ -68,8 +68,9 @@ export const selectTheme: Action = ({ storage.set('theme', theme) } -export const toggleTypescript: Action = ({ state }) => { +export const toggleTypescript: Action = ({ state, storage }) => { state.typescript = !state.typescript + storage.set('typescript', state.typescript) } export const closeSearch: Action = ({ state }) => { @@ -87,3 +88,9 @@ export const changeQuery: Operator< debounce(200), o.query ) + +export const viewHelpGotIt: Action = ({ state, storage }) => { + state.showViewHelp = false + storage.set('typescript', false) + storage.set('theme', 'react') +} diff --git a/packages/overmind-website/src/overmind/onInitialize.ts b/packages/overmind-website/src/overmind/onInitialize.ts index 68cb4dcf..1ec30101 100644 --- a/packages/overmind-website/src/overmind/onInitialize.ts +++ b/packages/overmind-website/src/overmind/onInitialize.ts @@ -9,6 +9,7 @@ const onInitialize: OnInitialize = ({ }) => { state.typescript = storage.get('typescript') || false state.theme = storage.get('theme') || 'react' + state.showViewHelp = !storage.get('theme') css.changePrimary(state.theme) router.route('/', app.actions.openHome) diff --git a/packages/overmind-website/src/overmind/state.ts b/packages/overmind-website/src/overmind/state.ts index fc4ecae5..913621d6 100644 --- a/packages/overmind-website/src/overmind/state.ts +++ b/packages/overmind-website/src/overmind/state.ts @@ -27,6 +27,7 @@ type State = { isLoadingGuides: boolean isLoadingApis: boolean isLoadingVideos: boolean + showViewHelp: boolean } const state: State = { @@ -48,6 +49,7 @@ const state: State = { isLoadingGuides: false, isLoadingApis: false, isLoadingVideos: false, + showViewHelp: false, } export default state