├── src ├── react-app-env.d.ts ├── examples │ ├── 1-react-router │ │ ├── 6-bonus-other-adts │ │ │ ├── types.d.ts │ │ │ ├── fp-ts-codegen.ts │ │ │ ├── Daggy.ts │ │ │ ├── Morphic-ts-adt.ts │ │ │ ├── Unionize.ts │ │ │ ├── FoldableHelpers.ts │ │ │ └── CodegenLocation.ts │ │ ├── common │ │ │ └── Link.tsx │ │ ├── 3-union-types │ │ │ ├── PathnameLink.tsx │ │ │ └── App.tsx │ │ ├── 6-morphic-ts │ │ │ ├── LocationLink.tsx │ │ │ ├── code.ts │ │ │ └── App.tsx │ │ ├── 5-fp-ts-routing │ │ │ ├── LocationLink.tsx │ │ │ ├── code.ts │ │ │ └── App.tsx │ │ ├── 4-tagged-unions │ │ │ ├── LocationLink.tsx │ │ │ └── App.tsx │ │ ├── 7-morphic-ts-routing │ │ │ ├── LocationLink.tsx │ │ │ ├── code.ts │ │ │ └── App.tsx │ │ ├── 1-pathname-anchor │ │ │ └── App.tsx │ │ ├── 2-pushstate-onpopstate │ │ │ └── App.tsx │ │ ├── 2-bonus-hash-history-state │ │ │ └── App.tsx │ │ ├── 5-bonus-ints-queries-types │ │ │ └── code.ts │ │ └── 6-bonus-verified │ │ │ └── App.tsx │ ├── optional-next-js │ │ ├── 1-router │ │ │ ├── clientAppElement.tsx │ │ │ ├── handleRoute.tsx │ │ │ ├── NodeSafeWindow.ts │ │ │ └── App.tsx │ │ ├── 2-flicker │ │ │ ├── clientAppElement.tsx │ │ │ ├── handleRoute.tsx │ │ │ └── App.tsx │ │ ├── 5-hydration-safe-fetch │ │ │ ├── clientAppElement.tsx │ │ │ ├── NodeSafeWindow.ts │ │ │ ├── handleRoute.tsx │ │ │ └── App.tsx │ │ ├── 3-hydration-safe-url │ │ │ └── clientAppElement.tsx │ │ └── 4-data-fetch │ │ │ ├── clientAppElement.tsx │ │ │ ├── handleRoute.tsx │ │ │ └── App.tsx │ ├── 2-data-model │ │ ├── 5-test │ │ │ ├── App.tsx │ │ │ ├── todorouter.test.tsx │ │ │ └── MockableApp.tsx │ │ ├── 1-simple-todo │ │ │ ├── AppState.ts │ │ │ ├── components │ │ │ │ ├── Link.tsx │ │ │ │ ├── Todo.tsx │ │ │ │ ├── AddTodo.tsx │ │ │ │ ├── TodoList.tsx │ │ │ │ └── Footer.tsx │ │ │ └── App.tsx │ │ ├── 3-router │ │ │ ├── Location.ts │ │ │ └── App.tsx │ │ ├── 4-ssot │ │ │ ├── Location.ts │ │ │ └── App.tsx │ │ └── 2-react-testing-library │ │ │ └── todo.test.tsx │ └── 3-reactive-architecture-w-redux │ │ ├── 1-redux │ │ ├── 2-store-reducer │ │ │ ├── AppAction.ts │ │ │ ├── App.tsx │ │ │ ├── reducer.ts │ │ │ └── ReduxApp.tsx │ │ ├── 3-morphic-ts │ │ │ ├── AppAction.ts │ │ │ ├── App.tsx │ │ │ ├── ReduxApp.tsx │ │ │ └── reducer.ts │ │ ├── 1-global-state │ │ │ ├── AppState.ts │ │ │ └── App.tsx │ │ ├── bonus-monocle-ts │ │ │ ├── App.tsx │ │ │ └── reducer.ts │ │ ├── 5-router-attempt │ │ │ ├── App.tsx │ │ │ ├── ReduxApp.tsx │ │ │ └── reducer.ts │ │ └── 4-test │ │ │ └── reducer.test.ts │ │ ├── 3-redux-observable │ │ ├── 1-popstate │ │ │ ├── epic.ts │ │ │ └── App.tsx │ │ ├── 2-pushState │ │ │ ├── AppAction.ts │ │ │ ├── App.tsx │ │ │ ├── epic.ts │ │ │ ├── ReduxApp.tsx │ │ │ └── reducer.ts │ │ └── 3-test │ │ │ ├── 1-pushState │ │ │ ├── epic.ts │ │ │ ├── App.tsx │ │ │ └── todorouter.test.tsx │ │ │ └── 2-onpopstate │ │ │ └── todorouter.test.tsx │ │ └── 2-rxjs │ │ ├── 2-rxjs │ │ └── observable.ts │ │ ├── 1-currying │ │ ├── bonus-flow.ts │ │ └── 1-piping-currying.ts │ │ └── bonus-test-router │ │ ├── App.tsx │ │ ├── todorouter.test.tsx │ │ └── reducer.ts ├── setupTests.ts ├── index.tsx └── serviceWorker.ts ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── .github └── ISSUE_TEMPLATE │ └── youtube-comment-issue-template.md ├── server ├── hydrate.tsx ├── server.tsx ├── index.html └── GenerateClient.ts ├── .gitignore ├── tsconfig.json ├── server.tsconfig.json ├── .vscode └── launch.json ├── README.md ├── package.json └── webpack.server.js /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/examples/1-react-router/6-bonus-other-adts/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'daggy'; -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonyjoeseph/fp-ts-routing-examples/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonyjoeseph/fp-ts-routing-examples/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anthonyjoeseph/fp-ts-routing-examples/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/examples/optional-next-js/1-router/clientAppElement.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from './App'; 3 | 4 | const clientAppElement = (): React.ReactElement => ( 5 | 6 | ); 7 | 8 | export default clientAppElement; -------------------------------------------------------------------------------- /src/examples/2-data-model/5-test/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MockableApp from "./MockableApp"; 3 | 4 | 5 | export const App = () => ( 6 | window.history.pushState(null, '', url) } 8 | /> 9 | ); -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/youtube-comment-issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Youtube comment issue template 3 | about: 'Provides ' 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | This comment is for video: [insert video title here] 11 | -------------------------------------------------------------------------------- /src/examples/optional-next-js/2-flicker/clientAppElement.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from './App'; 3 | 4 | const clientAppElement = (): React.ReactElement => ( 5 | 8 | ); 9 | 10 | export default clientAppElement; -------------------------------------------------------------------------------- /server/hydrate.tsx: -------------------------------------------------------------------------------- 1 | // server/hydrate.tsx 2 | 3 | import ReactDOM from 'react-dom'; 4 | import clientAppElement from '../src/examples/optional-next-js/5-hydration-safe-fetch/clientAppElement'; 5 | 6 | ReactDOM.hydrate( 7 | clientAppElement(), 8 | document.getElementById('root') 9 | ); -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /src/examples/optional-next-js/5-hydration-safe-fetch/clientAppElement.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from './App'; 3 | 4 | const clientAppElement = (): React.ReactElement => ( 5 | 8 | ); 9 | 10 | export default clientAppElement; -------------------------------------------------------------------------------- /src/examples/optional-next-js/3-hydration-safe-url/clientAppElement.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from '../2-flicker/App'; 3 | 4 | const clientAppElement = (): React.ReactElement => ( 5 | 8 | ); 9 | 10 | export default clientAppElement; -------------------------------------------------------------------------------- /src/examples/optional-next-js/4-data-fetch/clientAppElement.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from './App'; 3 | 4 | const clientAppElement = (): React.ReactElement => ( 5 | 9 | ); 10 | 11 | export default clientAppElement; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | server-build -------------------------------------------------------------------------------- /src/examples/1-react-router/common/Link.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Link = ({ 4 | to, 5 | updatePathname, 6 | children, 7 | }: { 8 | to: string; 9 | updatePathname: (to: string) => void; 10 | children: string; 11 | }) => ( 12 | { 15 | event.preventDefault(); 16 | updatePathname(to); 17 | }} 18 | > 19 | {children} 20 | 21 | ); 22 | 23 | export default Link; -------------------------------------------------------------------------------- /src/examples/2-data-model/1-simple-todo/AppState.ts: -------------------------------------------------------------------------------- 1 | export interface TodoType { 2 | id: number; 3 | text: string; 4 | completed: boolean; 5 | } 6 | export type VisibilityFilter = 'SHOW_ALL' | 'SHOW_ACTIVE' | 'SHOW_COMPLETED' 7 | 8 | export const defaultTodos: TodoType[] = [ 9 | { 10 | id: 0, 11 | text: 'Learn fp-ts-routing', 12 | completed: false, 13 | }, 14 | { 15 | id: 1, 16 | text: 'Go Shopping', 17 | completed: false, 18 | } 19 | ] -------------------------------------------------------------------------------- /src/examples/2-data-model/3-router/Location.ts: -------------------------------------------------------------------------------- 1 | import { end, lit } from 'fp-ts-routing'; 2 | import { routingFromMatches3 } from 'morphic-ts-routing'; 3 | import { ADTType } from '@morphic-ts/adt'; 4 | 5 | export const { 6 | parse, 7 | format, 8 | adt: Location, 9 | } = routingFromMatches3( 10 | ['Landing', end], 11 | ['Active', lit('active').then(end)], 12 | ['Completed', lit('completed').then(end)], 13 | ); 14 | export type Location = ADTType -------------------------------------------------------------------------------- /src/examples/2-data-model/1-simple-todo/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from 'react'; 2 | 3 | const Link: FunctionComponent<{ 4 | active: boolean; 5 | onClick: () => void; 6 | }> = ({ 7 | active, 8 | onClick, 9 | children, 10 | }) => ( 11 | 20 | ) 21 | 22 | export default Link; -------------------------------------------------------------------------------- /src/examples/2-data-model/1-simple-todo/components/Todo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Todo = ({ 4 | onClick, 5 | completed, 6 | text, 7 | }: { 8 | onClick: () => void; 9 | completed: boolean; 10 | text: string; 11 | }) => ( 12 |
  • 19 | {text} 20 |
  • 21 | ) 22 | 23 | export default Todo; -------------------------------------------------------------------------------- /src/examples/1-react-router/3-union-types/PathnameLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from '../common/Link'; 3 | import { Pathname } from './App'; 4 | 5 | const PathnameLink = ({ 6 | to, 7 | updatePathname, 8 | children, 9 | }: { 10 | to: Pathname; 11 | updatePathname: (pathname: Pathname) => void; 12 | children: string; 13 | }) => updatePathname(to)} 16 | children={children} 17 | />; 18 | 19 | export default PathnameLink; -------------------------------------------------------------------------------- /src/examples/1-react-router/6-morphic-ts/LocationLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from '../common/Link'; 3 | import { Location, format } from './App'; 4 | 5 | const LocationLink = ({ 6 | to, 7 | updateLocation, 8 | children, 9 | }: { 10 | to: Location; 11 | updateLocation: (location: Location) => void; 12 | children: string; 13 | }) => updateLocation(to)} 16 | children={children} 17 | />; 18 | 19 | export default LocationLink; -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/2-store-reducer/AppAction.ts: -------------------------------------------------------------------------------- 1 | import { VisibilityFilter } from "../1-global-state/AppState"; 2 | 3 | export interface ADD_TODO { 4 | type: 'ADD_TODO', 5 | text: string; 6 | } 7 | export interface SET_VISIBILITY_FILTER { 8 | type: 'SET_VISIBILITY_FILTER', 9 | filter: VisibilityFilter, 10 | } 11 | export interface TOGGLE_TODO { 12 | type: 'TOGGLE_TODO', 13 | id: number; 14 | } 15 | 16 | export type AppAction = ADD_TODO | SET_VISIBILITY_FILTER | TOGGLE_TODO -------------------------------------------------------------------------------- /src/examples/1-react-router/5-fp-ts-routing/LocationLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from '../common/Link'; 3 | import { Location, format } from './App'; 4 | 5 | const LocationLink = ({ 6 | to, 7 | updateLocation, 8 | children, 9 | }: { 10 | to: Location; 11 | updateLocation: (location: Location) => void; 12 | children: string; 13 | }) => updateLocation(to)} 16 | children={children} 17 | />; 18 | 19 | export default LocationLink; -------------------------------------------------------------------------------- /src/examples/1-react-router/4-tagged-unions/LocationLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from '../common/Link'; 3 | import { Location, format } from './App'; 4 | 5 | const LocationLink = ({ 6 | to, 7 | updateLocation, 8 | children, 9 | }: { 10 | to: Location; 11 | updateLocation: (location: Location) => void; 12 | children: string; 13 | }) => updateLocation(to)} 16 | children={children} 17 | />; 18 | 19 | export default LocationLink; 20 | -------------------------------------------------------------------------------- /src/examples/1-react-router/7-morphic-ts-routing/LocationLink.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Link from '../common/Link'; 3 | import { Location, format } from './App'; 4 | 5 | const LocationLink = ({ 6 | to, 7 | updateLocation, 8 | children, 9 | }: { 10 | to: Location; 11 | updateLocation: (location: Location) => void; 12 | children: string; 13 | }) => updateLocation(to)} 16 | children={children} 17 | />; 18 | 19 | export default LocationLink; -------------------------------------------------------------------------------- /server/server.tsx: -------------------------------------------------------------------------------- 1 | // server/server.tsx 2 | 3 | import express from 'express'; 4 | import handleRoute from '../src/examples/optional-next-js/5-hydration-safe-fetch/handleRoute'; 5 | 6 | const PORT = process.env.PORT || 3006; 7 | const app = express(); 8 | 9 | app.use(express.static('./public', { 10 | index: false, 11 | })); 12 | 13 | app.use('/server-build', express.static('./server-build')); 14 | 15 | app.get('*', handleRoute); 16 | 17 | app.listen(PORT, () => { 18 | console.log(`😎 Server is listening on port ${PORT}`); 19 | }); -------------------------------------------------------------------------------- /src/examples/1-react-router/6-bonus-other-adts/fp-ts-codegen.ts: -------------------------------------------------------------------------------- 1 | import * as C from './CodegenLocation'; 2 | 3 | const codegenFormatter = C.fold({ 4 | onHome: () => '/', 5 | onAbout: () => '/about', 6 | onTopics: () => '/topics', 7 | onTopicsID: id => `/topics/${id}`, 8 | onNotFound: () => '/', 9 | }); 10 | 11 | const codegenLocations: C.Location[] = [ 12 | C.home, 13 | C.about, 14 | C.topics, 15 | C.topicsID('someid'), 16 | ]; 17 | codegenLocations.map(codegenFormatter).forEach(urls => { 18 | console.log(`formatted url: ${urls}`); 19 | }); 20 | -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/2-store-reducer/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { defaultAppState } from '../1-global-state/AppState'; 3 | import { createStore } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { reducer } from './reducer'; 6 | import ReduxApp from './ReduxApp'; 7 | 8 | let store = createStore( 9 | reducer, 10 | defaultAppState, 11 | ); 12 | 13 | const App = () => ( 14 | 17 | 18 | 19 | ); 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/3-morphic-ts/AppAction.ts: -------------------------------------------------------------------------------- 1 | import { VisibilityFilter } from "../1-global-state/AppState"; 2 | import { makeADT, ofType, ADTType } from "@morphic-ts/adt"; 3 | 4 | export const AppAction = makeADT('type')({ 5 | ADD_TODO: ofType<{ type: 'ADD_TODO'; text: string }>(), 6 | SET_VISIBILITY_FILTER: ofType<{ 7 | type: 'SET_VISIBILITY_FILTER', 8 | filter: VisibilityFilter, 9 | }>(), 10 | TOGGLE_TODO: ofType<{ 11 | type: 'TOGGLE_TODO', 12 | id: number; 13 | }>(), 14 | }); 15 | 16 | export type AppAction = ADTType -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './examples/1-react-router/7-morphic-ts-routing/App'; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | // If you want your app to work offline and load faster, you can change 14 | // unregister() to register() below. Note this comes with some pitfalls. 15 | // Learn more about service workers: https://bit.ly/CRA-PWA 16 | serviceWorker.unregister(); 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "jsx": "react" 21 | }, 22 | "include": [ 23 | "src", 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/3-morphic-ts/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { defaultAppState } from '../1-global-state/AppState'; 3 | import { createStore } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { safeReducer, curriedReducer } from './reducer'; 6 | import ReduxApp from './ReduxApp'; 7 | import { AppAction } from './AppAction'; 8 | 9 | let store = createStore( 10 | safeReducer(curriedReducer, defaultAppState, AppAction), 11 | defaultAppState, 12 | ); 13 | 14 | const App = () => ( 15 | 18 | 19 | 20 | ); 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /server.tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "outDir": "./dist/", 20 | "jsx": "react", 21 | 22 | 23 | "noEmit": false, 24 | "sourceMap": true 25 | }, 26 | "include": [ 27 | "server", 28 | "src/react-app-env.d.ts" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/1-global-state/AppState.ts: -------------------------------------------------------------------------------- 1 | export interface TodoType { 2 | id: number; 3 | text: string; 4 | completed: boolean; 5 | } 6 | export type VisibilityFilter = 'SHOW_ALL' | 'SHOW_ACTIVE' | 'SHOW_COMPLETED' 7 | 8 | export const defaultTodos: TodoType[] = [ 9 | { 10 | id: 0, 11 | text: 'Learn fp-ts-routing', 12 | completed: false, 13 | }, 14 | { 15 | id: 1, 16 | text: 'Go Shopping', 17 | completed: false, 18 | } 19 | ]; 20 | 21 | export interface AppState { 22 | todos: TodoType[]; 23 | visibilityFilter: VisibilityFilter; 24 | } 25 | 26 | export const defaultAppState: AppState = { 27 | visibilityFilter: 'SHOW_ALL', 28 | todos: defaultTodos, 29 | }; -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/bonus-monocle-ts/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { defaultAppState, AppState } from '../1-global-state/AppState'; 3 | import { createStore } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { AppAction } from '../3-morphic-ts/AppAction'; 6 | import { safeReducer, curriedReducer } from './reducer'; 7 | import ReduxApp from '../3-morphic-ts/ReduxApp'; 8 | 9 | let store = createStore( 10 | safeReducer(curriedReducer, defaultAppState, AppAction), 11 | defaultAppState, 12 | ); 13 | 14 | const App = () => ( 15 | 18 | 19 | 20 | ); 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /src/examples/1-react-router/6-bonus-other-adts/Daggy.ts: -------------------------------------------------------------------------------- 1 | import * as daggy from 'daggy'; 2 | 3 | const Location = daggy.taggedSum('Location', { 4 | Home: [], 5 | About: [], 6 | Topics: [], 7 | TopicsID: ['id'], 8 | NotFound: [], 9 | }); 10 | 11 | Location.prototype.format = function() { 12 | return this.cata({ 13 | Home: () => '/', 14 | About: () => '/about', 15 | Topics: () => '/topics', 16 | TopicsID: (id: string) => `/topics/${id}`, 17 | NotFound: () => '/', 18 | }); 19 | }; 20 | 21 | const urls = [ 22 | Location.Home, 23 | Location.About, 24 | Location.Topics, 25 | Location.TopicsID('someid'), 26 | ]; 27 | urls 28 | .map(l => l.format()) 29 | .forEach(urls => { 30 | console.log(`formatted url: ${urls}`); 31 | }); 32 | -------------------------------------------------------------------------------- /server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | React App 17 | 18 | 19 | 20 |
    21 | 22 | 23 | -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/3-redux-observable/1-popstate/epic.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/lib/pipeable'; 2 | import * as r from 'rxjs'; 3 | import * as ro from 'rxjs/operators'; 4 | import { Epic } from 'redux-observable'; 5 | import { AppAction } from '../../1-redux/3-morphic-ts/AppAction'; 6 | import { AppState } from '../../1-redux/1-global-state/AppState'; 7 | import { parse, routeToVisibilityFilter } from '../../../2-data-model/4-ssot/Location'; 8 | 9 | const epic: Epic = () => pipe( 10 | r.fromEvent(window, 'popstate'), 11 | ro.map(() => AppAction.of.SET_VISIBILITY_FILTER({ 12 | filter: pipe( 13 | window.location.pathname, 14 | parse, 15 | routeToVisibilityFilter 16 | ), 17 | })), 18 | ) 19 | 20 | export default epic; -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/2-rxjs/2-rxjs/observable.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/lib/pipeable'; 2 | import * as r from 'rxjs'; 3 | import * as ro from 'rxjs/operators'; 4 | 5 | const observable = new r.Observable(subscriber => { 6 | subscriber.next(1); 7 | subscriber.next(2); 8 | subscriber.next(3); 9 | setTimeout(() => { 10 | subscriber.next(4); 11 | subscriber.complete(); 12 | }, 1000); 13 | }); 14 | 15 | observable.subscribe(console.log); 16 | 17 | const interpolated = pipe( 18 | observable, 19 | ro.map(String), 20 | ro.map(n => `${n} with string interpolation`) 21 | ); 22 | 23 | interpolated.subscribe(console.log); 24 | 25 | const sameThing = observable.pipe( 26 | ro.map(String), 27 | ro.map(n => `${n} with string interpolation`) 28 | ); 29 | 30 | sameThing.subscribe(console.log); 31 | -------------------------------------------------------------------------------- /src/examples/1-react-router/6-bonus-other-adts/Morphic-ts-adt.ts: -------------------------------------------------------------------------------- 1 | import { makeADT, ofType, ADTType } from '@morphic-ts/adt'; 2 | 3 | const Location = makeADT('type')({ 4 | Home: ofType(), 5 | About: ofType(), 6 | Topics: ofType(), 7 | TopicsID: ofType<{ type: 'TopicsID'; id: string }>(), 8 | NotFound: ofType(), 9 | }); 10 | type Location = ADTType; 11 | 12 | const format = Location.matchStrict({ 13 | Home: () => '/', 14 | About: () => '/about', 15 | Topics: () => '/topics', 16 | TopicsID: ({ id }) => `/topics/${id}`, 17 | NotFound: () => '/', 18 | }); 19 | 20 | const locations: Location[] = [ 21 | Location.of.Home({}), 22 | Location.of.About({}), 23 | Location.of.Topics({}), 24 | Location.of.TopicsID({ id: 'someid' }), 25 | ]; 26 | locations.map(format).forEach(urls => { 27 | console.log(`formatted url: ${urls}`); 28 | }); 29 | -------------------------------------------------------------------------------- /src/examples/2-data-model/1-simple-todo/components/AddTodo.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const AddTodo = ({ 4 | addTodo, 5 | }: { 6 | addTodo: (text: string) => void; 7 | }) => { 8 | const [text, setText] = useState(''); 9 | return ( 10 |
    11 |
    { 13 | e.preventDefault() 14 | if (!text.trim()) { 15 | return; 16 | } 17 | addTodo(text); 18 | }} 19 | > 20 | setText(e.target.value)} 25 | /> 26 | 32 |
    33 |
    34 | ) 35 | } 36 | 37 | export default AddTodo; -------------------------------------------------------------------------------- /src/examples/optional-next-js/1-router/handleRoute.tsx: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from 'express'; 2 | import type { ParamsDictionary } from 'express-serve-static-core'; 3 | import type QueryString from 'qs'; 4 | import React from 'react'; 5 | import generateClient from '../../../../server/GenerateClient'; 6 | import App from './App'; 7 | 8 | const handleRoute = async ( 9 | req: Request, 10 | res: Response, 11 | ) => { 12 | try { 13 | const clientString = await generateClient( 14 | , 15 | undefined 16 | ) 17 | return res.send(clientString); 18 | } catch (untypedErr) { 19 | const err: NodeJS.ErrnoException = untypedErr; 20 | console.error('Something went wrong:', err); 21 | return res.status(500).send('Oops, better luck next time!'); 22 | } 23 | }; 24 | 25 | export default handleRoute; -------------------------------------------------------------------------------- /src/examples/1-react-router/6-bonus-other-adts/Unionize.ts: -------------------------------------------------------------------------------- 1 | import * as U from 'unionize'; 2 | 3 | const Location = U.unionize( 4 | { 5 | Home: U.ofType(), 6 | About: U.ofType(), 7 | Topics: U.ofType(), 8 | TopicsID: U.ofType<{ id: string }>(), 9 | NotFound: U.ofType(), 10 | }, 11 | { value: 'value', tag: 'type' }, 12 | ); 13 | type Location = U.UnionOf; 14 | 15 | const format = Location.match({ 16 | Home: () => '/', 17 | About: () => '/about', 18 | Topics: () => '/topics', 19 | TopicsID: ({ id }) => `/topics/${id}`, 20 | NotFound: () => '/', 21 | }); 22 | 23 | const UnionizeLocations: Location[] = [ 24 | Location.Home(), 25 | Location.About(), 26 | Location.Topics(), 27 | Location.TopicsID({ id: 'someid' }), 28 | Location.Home(), 29 | ]; 30 | UnionizeLocations.map(format).forEach(urls => { 31 | console.log(`formatted url: ${urls}`); 32 | }); 33 | -------------------------------------------------------------------------------- /server/GenerateClient.ts: -------------------------------------------------------------------------------- 1 | import serialize from 'serialize-javascript'; 2 | import path from 'path'; 3 | import fs from 'fs'; 4 | import React from 'react'; 5 | import ReactDOMServer from 'react-dom/server'; 6 | 7 | export default

    ( 8 | App: React.ReactElement

    , 9 | globalState: string | undefined, 10 | ): Promise => new Promise((resolve, reject) => { 11 | const app = ReactDOMServer.renderToString(App); 12 | 13 | const indexFile = path.resolve('./server/index.html'); 14 | fs.readFile(indexFile, 'utf8', (err, data) => { 15 | if (err) { 16 | reject(err); 17 | } 18 | const clientString = data 19 | .replace( 20 | '

    ', 21 | `
    ${app}
    `, 22 | ) 23 | .replace( 24 | '', 25 | ``, 26 | ); 27 | resolve(clientString); 28 | }); 29 | }) -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/5-router-attempt/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { defaultAppState } from '../1-global-state/AppState'; 3 | import { createStore } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { safeReducer, curriedReducer } from './reducer'; 6 | import ReduxApp from './ReduxApp'; 7 | import { AppAction } from '../3-morphic-ts/AppAction'; 8 | import { routeToVisibilityFilter, parse } from '../../../2-data-model/4-ssot/Location'; 9 | 10 | let store = createStore( 11 | safeReducer(curriedReducer, { 12 | ...defaultAppState, 13 | visibilityFilter: routeToVisibilityFilter( 14 | parse( 15 | window.location.pathname 16 | ) 17 | ), 18 | }, AppAction), 19 | defaultAppState, 20 | ); 21 | 22 | const App = () => ( 23 | 26 | 27 | 28 | ); 29 | 30 | export default App; 31 | -------------------------------------------------------------------------------- /src/examples/optional-next-js/2-flicker/handleRoute.tsx: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from 'express'; 2 | import type { ParamsDictionary } from 'express-serve-static-core'; 3 | import type QueryString from 'qs'; 4 | import React from 'react'; 5 | import generateClient from '../../../../server/GenerateClient'; 6 | import App from './App'; 7 | 8 | const handleRoute = async ( 9 | req: Request, 10 | res: Response, 11 | ) => { 12 | try { 13 | const clientString = await generateClient( 14 | , 17 | undefined 18 | ) 19 | return res.send(clientString); 20 | } catch (untypedErr) { 21 | const err: NodeJS.ErrnoException = untypedErr; 22 | console.error('Something went wrong:', err); 23 | return res.status(500).send('Oops, better luck next time!'); 24 | } 25 | }; 26 | 27 | export default handleRoute; -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/3-redux-observable/2-pushState/AppAction.ts: -------------------------------------------------------------------------------- 1 | import { VisibilityFilter } from "../../1-redux/1-global-state/AppState"; 2 | import { makeADT, ofType, ADTType, unionADT } from "@morphic-ts/adt"; 3 | 4 | export const TransformAction = makeADT('type')({ 5 | ADD_TODO: ofType<{ type: 'ADD_TODO'; text: string }>(), 6 | SET_VISIBILITY_FILTER: ofType<{ 7 | type: 'SET_VISIBILITY_FILTER', 8 | filter: VisibilityFilter, 9 | }>(), 10 | TOGGLE_TODO: ofType<{ 11 | type: 'TOGGLE_TODO', 12 | id: number; 13 | }>(), 14 | }); 15 | export type TransformAction = ADTType 16 | 17 | export const AppAction = unionADT([ 18 | TransformAction, 19 | makeADT('type')({ 20 | SET_VISIBILITY_FILTER_LOCATION: ofType<{ 21 | type: 'SET_VISIBILITY_FILTER_LOCATION', 22 | filter: VisibilityFilter, 23 | }>(), 24 | }) 25 | ]); 26 | 27 | export type AppAction = ADTType -------------------------------------------------------------------------------- /src/examples/optional-next-js/1-router/NodeSafeWindow.ts: -------------------------------------------------------------------------------- 1 | const safeWindow = { 2 | ...(typeof window !== 'undefined' 3 | ? { 4 | ...window, 5 | addEventListener: ( 6 | type: K, 7 | listener: (this: Window, ev: WindowEventMap[K]) => any, 8 | options?: boolean | AddEventListenerOptions) => { 9 | // addEventListener is bound to 'window' strangely, 10 | // must be invoked in this way 11 | window.addEventListener(type, listener, options); 12 | }, 13 | } 14 | : { 15 | addEventListener: ( 16 | type: K, 17 | listener: (this: Window, ev: WindowEventMap[K]) => any, 18 | options?: boolean | AddEventListenerOptions) => {}, 19 | location: { 20 | pathname: '', 21 | }, 22 | history: { 23 | pushState: (data: any, title: string, url?: string | null) => {}, 24 | } 25 | }) 26 | } 27 | 28 | export default safeWindow; -------------------------------------------------------------------------------- /src/examples/2-data-model/1-simple-todo/components/TodoList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Todo from './Todo'; 3 | import { VisibilityFilter, TodoType } from '../AppState'; 4 | 5 | const TodoList = ({ 6 | todos, 7 | visibility, 8 | toggleTodo 9 | }: { 10 | todos: TodoType[]; 11 | visibility: VisibilityFilter; 12 | toggleTodo: (id: number) => void; 13 | }) => { 14 | let visibileTodos: TodoType[]; 15 | switch (visibility) { 16 | case 'SHOW_ALL': 17 | visibileTodos = todos; 18 | break; 19 | case 'SHOW_ACTIVE': 20 | visibileTodos = todos.filter(t => !t.completed); 21 | break; 22 | case 'SHOW_COMPLETED': 23 | visibileTodos = todos.filter(t => t.completed); 24 | break; 25 | } 26 | return ( 27 |
      28 | {visibileTodos.map(todo => ( 29 | toggleTodo(todo.id)} 33 | /> 34 | ))} 35 |
    36 | ) 37 | } 38 | 39 | export default TodoList; -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/2-rxjs/1-currying/bonus-flow.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/lib/pipeable'; 2 | import { flow } from 'fp-ts/lib/function'; 3 | 4 | const plus = (a: number, b: number) => a + b 5 | const curriedPlus = (a: number) => (b: number) => a + b 6 | 7 | const allOps = (input: number) => pipe( 8 | input, 9 | curriedPlus(28), 10 | Math.floor, 11 | String, 12 | Array.of, 13 | Array.isArray 14 | ) 15 | export const output1 = allOps(1234.5678); 16 | export const output2 = allOps(8765.4321); 17 | 18 | export const allOpsFlow = flow( 19 | curriedPlus(28), 20 | Math.floor, 21 | String, 22 | Array.of, 23 | Array.isArray 24 | ) 25 | export const flowOutput1 = allOpsFlow(1234.5678); 26 | export const flowOutput2 = allOpsFlow(8765.4321); 27 | 28 | export const multiparam = flow( 29 | plus, 30 | Math.floor, 31 | String, 32 | Array.of, 33 | Array.isArray 34 | ) 35 | export const multiparamOutput1 = multiparam(28, 1234.5678); 36 | export const multiparamOutput2 = multiparam(42, 8765.4321); -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Debug Create React App", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceFolder}" 13 | }, 14 | { 15 | "type": "chrome", 16 | "request": "launch", 17 | "name": "Debug SSR Client", 18 | "url": "http://localhost:3006", 19 | "webRoot": "${workspaceFolder}", 20 | "sourceMaps": true 21 | }, 22 | { 23 | "name": "Debug SSR Server", 24 | "type": "node", 25 | "request": "launch", 26 | "cwd": "${workspaceRoot}", 27 | "program": "${workspaceRoot}/server/server.tsx", 28 | "outFiles": [ 29 | "${workspaceRoot}/server-build/**/*.js" 30 | ], 31 | "sourceMaps": true 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /src/examples/optional-next-js/5-hydration-safe-fetch/NodeSafeWindow.ts: -------------------------------------------------------------------------------- 1 | const safeWindow = { 2 | __INITIAL__DATA__: '', 3 | ...(typeof window !== 'undefined' 4 | ? { 5 | ...window, 6 | addEventListener: ( 7 | type: K, 8 | listener: (this: Window, ev: WindowEventMap[K]) => any, 9 | options?: boolean | AddEventListenerOptions) => { 10 | // addEventListener is bound to 'window' strangely, 11 | // must be invoked in this way 12 | window.addEventListener(type, listener, options); 13 | }, 14 | } 15 | : { 16 | addEventListener: ( 17 | type: K, 18 | listener: (this: Window, ev: WindowEventMap[K]) => any, 19 | options?: boolean | AddEventListenerOptions) => {}, 20 | location: { 21 | href: '', 22 | host: '', 23 | protocol: '', 24 | pathname: '', 25 | }, 26 | history: { 27 | pushState: (data: any, title: string, url?: string | null) => {}, 28 | } 29 | }) 30 | } 31 | 32 | export default safeWindow; -------------------------------------------------------------------------------- /src/examples/2-data-model/1-simple-todo/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { VisibilityFilter } from '../AppState'; 3 | import Link from './Link'; 4 | 5 | const Footer = ({ 6 | visibilityFilter, 7 | setVisibilityFilter, 8 | }: { 9 | visibilityFilter: VisibilityFilter; 10 | setVisibilityFilter: (newVisibility: VisibilityFilter) => void; 11 | }) => ( 12 |
    13 | Show: 14 | setVisibilityFilter('SHOW_ALL')} 17 | active={visibilityFilter === 'SHOW_ALL'} 18 | > 19 | All 20 | 21 | setVisibilityFilter('SHOW_ACTIVE')} 24 | active={visibilityFilter === 'SHOW_ACTIVE'} 25 | > 26 | Active 27 | 28 | setVisibilityFilter('SHOW_COMPLETED')} 31 | active={visibilityFilter === 'SHOW_COMPLETED'} 32 | > 33 | Completed 34 | 35 |
    36 | ) 37 | 38 | export default Footer; -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/2-rxjs/bonus-test-router/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { defaultAppState } from '../../1-redux/1-global-state/AppState'; 3 | import { createStore } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { safeReducer, curriedReducer } from './reducer'; 6 | import ReduxApp from '../../1-redux/5-router-attempt/ReduxApp'; 7 | import { AppAction } from '../../1-redux/3-morphic-ts/AppAction'; 8 | import { routeToVisibilityFilter, parse } from '../../../2-data-model/4-ssot/Location'; 9 | 10 | let store = createStore( 11 | safeReducer( 12 | curriedReducer( 13 | url => window.history.pushState(null, '', url), 14 | ), 15 | { 16 | ...defaultAppState, 17 | visibilityFilter: routeToVisibilityFilter( 18 | parse( 19 | window.location.pathname 20 | ) 21 | ), 22 | }, 23 | AppAction 24 | ), 25 | defaultAppState, 26 | ); 27 | 28 | const App = () => ( 29 | 32 | 33 | 34 | ); 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /src/examples/optional-next-js/5-hydration-safe-fetch/handleRoute.tsx: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from 'express'; 2 | import type { ParamsDictionary } from 'express-serve-static-core'; 3 | import type QueryString from 'qs'; 4 | import fetch from 'node-fetch'; 5 | import React from 'react'; 6 | import generateClient from '../../../../server/GenerateClient'; 7 | import App from './App'; 8 | 9 | const handleRoute = async ( 10 | req: Request, 11 | res: Response, 12 | ) => { 13 | try { 14 | const fetched = await fetch('https://reqres.in/api/users?page=2') 15 | .then(resp => resp.text()) 16 | .then(text => `fetched: ${text}`); 17 | const clientString = await generateClient( 18 | , 21 | fetched 22 | ) 23 | return res.send(clientString); 24 | } catch (untypedErr) { 25 | const err: NodeJS.ErrnoException = untypedErr; 26 | console.error('Something went wrong:', err); 27 | return res.status(500).send('Oops, better luck next time!'); 28 | } 29 | }; 30 | 31 | export default handleRoute; -------------------------------------------------------------------------------- /src/examples/2-data-model/4-ssot/Location.ts: -------------------------------------------------------------------------------- 1 | import { end, lit } from 'fp-ts-routing'; 2 | import { routingFromMatches3 } from 'morphic-ts-routing'; 3 | import { ADTType } from '@morphic-ts/adt'; 4 | import { VisibilityFilter } from '../1-simple-todo/AppState'; 5 | 6 | export const { 7 | parse, 8 | format, 9 | adt: Location, 10 | } = routingFromMatches3( 11 | ['Landing', end], 12 | ['Active', lit('active').then(end)], 13 | ['Completed', lit('completed').then(end)], 14 | ); 15 | export type Location = ADTType 16 | 17 | export const routeToVisibilityFilter = Location.matchStrict({ 18 | Landing: () => 'SHOW_ALL', 19 | Active: () => 'SHOW_ACTIVE', 20 | Completed: () => 'SHOW_COMPLETED', 21 | NotFound: () => 'SHOW_ALL', 22 | }) 23 | 24 | export const visibilityFilterToRoute = (visibility: VisibilityFilter): Location => { 25 | switch (visibility) { 26 | case 'SHOW_ALL': 27 | return Location.of.Landing({ value: {} }); 28 | case 'SHOW_ACTIVE': 29 | return Location.of.Active({ value: {} }); 30 | case 'SHOW_COMPLETED': 31 | return Location.of.Completed({ value: {} }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/examples/optional-next-js/4-data-fetch/handleRoute.tsx: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from 'express'; 2 | import type { ParamsDictionary } from 'express-serve-static-core'; 3 | import type QueryString from 'qs'; 4 | import fetch from 'node-fetch'; 5 | import React from 'react'; 6 | import generateClient from '../../../../server/GenerateClient'; 7 | import App from './App'; 8 | 9 | const handleRoute = async ( 10 | req: Request, 11 | res: Response, 12 | ) => { 13 | try { 14 | const fetched = await fetch('https://reqres.in/api/users?page=2') 15 | .then(resp => resp.text()) 16 | .then(text => `fetched: ${text}`); 17 | const clientString = await generateClient( 18 | , 22 | undefined 23 | ) 24 | return res.send(clientString); 25 | } catch (untypedErr) { 26 | const err: NodeJS.ErrnoException = untypedErr; 27 | console.error('Something went wrong:', err); 28 | return res.status(500).send('Oops, better luck next time!'); 29 | } 30 | }; 31 | 32 | export default handleRoute; -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/3-redux-observable/2-pushState/App.tsx: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { createEpicMiddleware } from 'redux-observable'; 3 | import React from 'react'; 4 | import { Provider } from 'react-redux'; 5 | import ReduxApp from './ReduxApp'; 6 | import { safeReducer, curriedReducer } from './reducer'; 7 | import { defaultAppState, AppState } from '../../1-redux/1-global-state/AppState'; 8 | import { AppAction } from './AppAction'; 9 | import { routeToVisibilityFilter, parse } from '../../../2-data-model/4-ssot/Location'; 10 | import epic from './epic'; 11 | 12 | const epicMiddleware = createEpicMiddleware(); 13 | 14 | let store = createStore( 15 | safeReducer( 16 | curriedReducer, 17 | { 18 | ...defaultAppState, 19 | visibilityFilter: routeToVisibilityFilter( 20 | parse( 21 | window.location.pathname 22 | ) 23 | ), 24 | }, 25 | AppAction, 26 | ), 27 | defaultAppState, 28 | applyMiddleware( 29 | epicMiddleware, 30 | ), 31 | ); 32 | 33 | epicMiddleware.run(epic); 34 | 35 | const App = () => ( 36 | 39 | 40 | 41 | ); 42 | 43 | export default App; 44 | -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/3-redux-observable/2-pushState/epic.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/lib/pipeable'; 2 | import * as r from 'rxjs'; 3 | import * as ro from 'rxjs/operators'; 4 | import { Epic } from 'redux-observable'; 5 | import { AppAction } from './AppAction'; 6 | import { AppState } from '../../1-redux/1-global-state/AppState'; 7 | import { 8 | visibilityFilterToRoute, 9 | routeToVisibilityFilter, 10 | parse, 11 | format, 12 | } from '../../../2-data-model/4-ssot/Location'; 13 | 14 | const epic: Epic = ( 15 | action$, 16 | ) => r.merge( 17 | pipe( 18 | r.fromEvent(window, 'popstate'), 19 | ro.map(() => pipe( 20 | window.location.pathname, 21 | parse, 22 | routeToVisibilityFilter 23 | )), 24 | ro.map(filter => AppAction.of.SET_VISIBILITY_FILTER({ 25 | filter, 26 | })), 27 | ), 28 | pipe( 29 | action$, 30 | ro.filter(AppAction.is.SET_VISIBILITY_FILTER_LOCATION), 31 | ro.tap(({ filter }) => pipe( 32 | filter, 33 | visibilityFilterToRoute, 34 | format, 35 | url => window.history.pushState(null, '', url), 36 | )), 37 | ro.map(({ filter }) => AppAction.of.SET_VISIBILITY_FILTER({ 38 | filter, 39 | })), 40 | ), 41 | ); 42 | 43 | export default epic; -------------------------------------------------------------------------------- /src/examples/2-data-model/5-test/todorouter.test.tsx: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import React from 'react'; 3 | import { fireEvent, render, screen } from '@testing-library/react'; 4 | import MockableApp from './MockableApp'; 5 | import { format, Location } from '../4-ssot/Location'; 6 | 7 | test('New filters change the url', () => { 8 | let allPushedLocations: string[] = []; 9 | 10 | render( 11 | { 13 | allPushedLocations.push(url) 14 | }} 15 | /> 16 | ); 17 | 18 | const showCompletedTodos = screen.getByText(/Completed/i); 19 | expect(showCompletedTodos).toBeInTheDocument(); 20 | const showActiveTodos = screen.getByText(/Active/i); 21 | expect(showActiveTodos).toBeInTheDocument(); 22 | const showAllTodos = screen.getByText(/All/i); 23 | expect(showAllTodos).toBeInTheDocument(); 24 | 25 | fireEvent.click(showCompletedTodos); 26 | fireEvent.click(showActiveTodos); 27 | fireEvent.click(showAllTodos); 28 | 29 | assert.deepStrictEqual( 30 | allPushedLocations, 31 | [ 32 | format( 33 | Location.of.Completed({ value: {} }) 34 | ), 35 | format( 36 | Location.of.Active({ value: {} }) 37 | ), 38 | format( 39 | Location.of.Landing({ value: {} }) 40 | ) 41 | ] 42 | ); 43 | 44 | }); -------------------------------------------------------------------------------- /src/examples/1-react-router/1-pathname-anchor/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | export default function App() { 4 | const [counter, setCounter] = useState(0); 5 | const { pathname } = window.location; 6 | let innerComponent: JSX.Element; 7 | switch (pathname) { 8 | case '/': 9 | innerComponent = ; 10 | break; 11 | case '/about': 12 | innerComponent = ; 13 | break; 14 | case '/users': 15 | innerComponent = ; 16 | break; 17 | default: 18 | innerComponent = ; 19 | break; 20 | } 21 | return ( 22 |
    23 | 28 | 41 | {innerComponent} 42 |
    43 | ); 44 | } 45 | 46 | function Home() { 47 | return

    Home

    ; 48 | } 49 | 50 | function About() { 51 | return

    About

    ; 52 | } 53 | 54 | function Users() { 55 | return

    Users

    ; 56 | } 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/examples/1-react-router/6-bonus-other-adts/FoldableHelpers.ts: -------------------------------------------------------------------------------- 1 | import { createFoldObject } from '@iadvize-oss/foldable-helpers'; 2 | 3 | interface Home { 4 | readonly type: 'Home'; 5 | } 6 | 7 | interface About { 8 | readonly type: 'About'; 9 | } 10 | 11 | interface Topics { 12 | readonly type: 'Topics'; 13 | } 14 | 15 | interface TopicsID { 16 | readonly type: 'TopicsID'; 17 | readonly id: string; 18 | } 19 | 20 | interface NotFound { 21 | readonly type: 'NotFound'; 22 | } 23 | 24 | type Location = Home | About | Topics | TopicsID | NotFound; 25 | 26 | const fold = createFoldObject({ 27 | Home: (l): l is Home => l.type === 'Home', 28 | About: (l): l is About => l.type === 'About', 29 | Topics: (l): l is Topics => l.type === 'Topics', 30 | TopicsID: (l): l is TopicsID => l.type === 'TopicsID', 31 | NotFound: (l): l is NotFound => l.type === 'NotFound', 32 | }); 33 | 34 | const format = fold({ 35 | Home: () => '/', 36 | About: () => '/about', 37 | Topics: () => '/topics', 38 | TopicsID: ({ id }) => `/topics/${id}`, 39 | NotFound: () => '/', 40 | }); 41 | 42 | const locations: Location[] = [ 43 | { type: 'Home' }, 44 | { type: 'About' }, 45 | { type: 'Topics' }, 46 | { type: 'TopicsID', id: 'someid' }, 47 | ]; 48 | locations.map(format).forEach(urls => { 49 | console.log(`formatted url: ${urls}`); 50 | }); 51 | -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/3-redux-observable/1-popstate/App.tsx: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { createEpicMiddleware } from 'redux-observable'; 3 | import React from 'react'; 4 | import { Provider } from 'react-redux'; 5 | import ReduxApp from '../../1-redux/3-morphic-ts/ReduxApp'; 6 | import { safeReducer, curriedReducer } from '../../1-redux/5-router-attempt/reducer'; 7 | import { defaultAppState, AppState } from '../../1-redux/1-global-state/AppState'; 8 | import { AppAction } from '../../1-redux/3-morphic-ts/AppAction'; 9 | import { routeToVisibilityFilter, parse } from '../../../2-data-model/4-ssot/Location'; 10 | import epic from './epic'; 11 | 12 | const epicMiddleware = createEpicMiddleware(); 13 | 14 | let store = createStore( 15 | safeReducer( 16 | curriedReducer, 17 | { 18 | ...defaultAppState, 19 | visibilityFilter: routeToVisibilityFilter( 20 | parse( 21 | window.location.pathname 22 | ) 23 | ), 24 | }, 25 | AppAction, 26 | ), 27 | defaultAppState, 28 | applyMiddleware( 29 | epicMiddleware, 30 | ), 31 | ); 32 | 33 | epicMiddleware.run(epic); 34 | 35 | const App = () => ( 36 | 39 | 40 | 41 | ); 42 | 43 | export default App; 44 | -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/3-redux-observable/3-test/1-pushState/epic.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/lib/pipeable'; 2 | import * as r from 'rxjs'; 3 | import * as ro from 'rxjs/operators'; 4 | import { Epic } from 'redux-observable'; 5 | import { AppAction } from '../../2-pushState/AppAction'; 6 | import { AppState } from '../../../1-redux/1-global-state/AppState'; 7 | import { 8 | visibilityFilterToRoute, 9 | routeToVisibilityFilter, 10 | parse, 11 | format, 12 | } from '../../../../2-data-model/4-ssot/Location'; 13 | 14 | const epic = ( 15 | pushUrl: (url: string) => void, 16 | popstate$: r.Observable, 17 | hrefThunk: () => string, 18 | ): Epic => ( 19 | action$, 20 | ) => r.merge( 21 | pipe( 22 | popstate$, 23 | ro.map(() => pipe( 24 | hrefThunk(), 25 | parse, 26 | routeToVisibilityFilter 27 | )), 28 | ro.map(filter => AppAction.of.SET_VISIBILITY_FILTER({ 29 | filter, 30 | })), 31 | ), 32 | pipe( 33 | action$, 34 | ro.filter(AppAction.is.SET_VISIBILITY_FILTER_LOCATION), 35 | ro.tap(({ filter }) => pipe( 36 | filter, 37 | visibilityFilterToRoute, 38 | format, 39 | pushUrl, 40 | )), 41 | ro.map(({ filter }) => { 42 | return AppAction.of.SET_VISIBILITY_FILTER({ 43 | filter, 44 | }); 45 | }), 46 | ), 47 | ); 48 | 49 | export default epic; -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/2-store-reducer/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from "redux"; 2 | import { AppState, defaultAppState, TodoType } from "../1-global-state/AppState"; 3 | import { AppAction } from "./AppAction"; 4 | 5 | export const reducer: Reducer = ( 6 | state, 7 | action 8 | ): AppState => { 9 | if (state === undefined) { 10 | return defaultAppState; 11 | } 12 | const { todos } = state; 13 | switch (action.type) { 14 | case 'ADD_TODO': 15 | const highestID = todos.length > 0 16 | ? todos.reduce( 17 | (acc, cur) => cur.id > acc.id 18 | ? cur 19 | : acc 20 | ).id 21 | : 0; 22 | const newTodo: TodoType = { 23 | id: highestID + 1, 24 | text: action.text, 25 | completed: false, 26 | } 27 | return { 28 | ...state, 29 | todos: [...todos, newTodo] 30 | }; 31 | case 'SET_VISIBILITY_FILTER': 32 | return { 33 | ...state, 34 | visibilityFilter: action.filter, 35 | }; 36 | case 'TOGGLE_TODO': 37 | const newTodos = todos.map( 38 | t => t.id === action.id 39 | ? { 40 | ...t, 41 | completed: !t.completed, 42 | } 43 | : t 44 | ); 45 | return { 46 | ...state, 47 | todos: newTodos, 48 | }; 49 | } 50 | }; -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/3-morphic-ts/ReduxApp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppState } from '../1-global-state/AppState'; 3 | import AddTodo from '../../../2-data-model/1-simple-todo/components/AddTodo'; 4 | import TodoList from '../../../2-data-model/1-simple-todo/components/TodoList'; 5 | import Footer from '../../../2-data-model/1-simple-todo/components/Footer'; 6 | import { useSelector, useDispatch } from 'react-redux'; 7 | import { AppAction } from './AppAction'; 8 | 9 | const ReduxApp = () => { 10 | const state = useSelector(s => s); 11 | const dispatch = useDispatch<(a: AppAction) => void>(); 12 | const { 13 | todos, 14 | visibilityFilter, 15 | } = state; 16 | return ( 17 |
    18 | { 20 | dispatch( 21 | AppAction.of.ADD_TODO({ text }) 22 | ) 23 | }} 24 | /> 25 | { 29 | dispatch( 30 | AppAction.of.TOGGLE_TODO({ id }) 31 | ); 32 | }} 33 | /> 34 |
    { 37 | dispatch( 38 | AppAction.of.SET_VISIBILITY_FILTER({ filter }) 39 | ); 40 | }} 41 | /> 42 |
    43 | ) 44 | } 45 | 46 | export default ReduxApp; -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/3-redux-observable/2-pushState/ReduxApp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppState } from '../../1-redux/1-global-state/AppState'; 3 | import AddTodo from '../../../2-data-model/1-simple-todo/components/AddTodo'; 4 | import TodoList from '../../../2-data-model/1-simple-todo/components/TodoList'; 5 | import Footer from '../../../2-data-model/1-simple-todo/components/Footer'; 6 | import { useSelector, useDispatch } from 'react-redux'; 7 | import { AppAction } from './AppAction'; 8 | 9 | const ReduxApp = () => { 10 | const state = useSelector(s => s); 11 | const dispatch = useDispatch<(a: AppAction) => void>(); 12 | const { 13 | todos, 14 | visibilityFilter, 15 | } = state; 16 | return ( 17 |
    18 | { 20 | dispatch( 21 | AppAction.of.ADD_TODO({ text }) 22 | ) 23 | }} 24 | /> 25 | { 29 | dispatch( 30 | AppAction.of.TOGGLE_TODO({ id }) 31 | ); 32 | }} 33 | /> 34 |
    { 37 | dispatch( 38 | AppAction.of.SET_VISIBILITY_FILTER_LOCATION({ filter }) 39 | ); 40 | }} 41 | /> 42 |
    43 | ) 44 | } 45 | 46 | export default ReduxApp; -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/3-redux-observable/3-test/1-pushState/App.tsx: -------------------------------------------------------------------------------- 1 | import * as r from 'rxjs'; 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import { createEpicMiddleware } from 'redux-observable'; 4 | import React from 'react'; 5 | import { Provider } from 'react-redux'; 6 | import ReduxApp from '../../2-pushState/ReduxApp'; 7 | import { safeReducer, curriedReducer } from '../../2-pushState/reducer'; 8 | import { defaultAppState, AppState } from '../../../1-redux/1-global-state/AppState'; 9 | import { AppAction } from '../../2-pushState/AppAction'; 10 | import { routeToVisibilityFilter, parse } from '../../../../2-data-model/4-ssot/Location'; 11 | import epic from './epic'; 12 | 13 | const epicMiddleware = createEpicMiddleware(); 14 | 15 | let store = createStore( 16 | safeReducer( 17 | curriedReducer, 18 | { 19 | ...defaultAppState, 20 | visibilityFilter: routeToVisibilityFilter( 21 | parse( 22 | window.location.pathname 23 | ) 24 | ), 25 | }, 26 | AppAction, 27 | ), 28 | defaultAppState, 29 | applyMiddleware( 30 | epicMiddleware, 31 | ), 32 | ); 33 | 34 | epicMiddleware.run( 35 | epic( 36 | url => window.history.pushState(null, '', url), 37 | r.fromEvent(window, 'popstate'), 38 | () => window.location.pathname, 39 | ) 40 | ); 41 | 42 | const App = () => ( 43 | 46 | 47 | 48 | ); 49 | 50 | export default App; 51 | -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/2-store-reducer/ReduxApp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppState } from '../1-global-state/AppState'; 3 | import AddTodo from '../../../2-data-model/1-simple-todo/components/AddTodo'; 4 | import TodoList from '../../../2-data-model/1-simple-todo/components/TodoList'; 5 | import Footer from '../../../2-data-model/1-simple-todo/components/Footer'; 6 | import { useSelector, useDispatch } from 'react-redux'; 7 | import { AppAction } from './AppAction'; 8 | 9 | const ReduxApp = () => { 10 | const state = useSelector(s => s); 11 | const dispatch = useDispatch<(a: AppAction) => void>(); 12 | const { 13 | todos, 14 | visibilityFilter, 15 | } = state; 16 | return ( 17 |
    18 | { 20 | dispatch({ 21 | type: 'ADD_TODO', 22 | text, 23 | }) 24 | }} 25 | /> 26 | { 30 | dispatch({ 31 | type: 'TOGGLE_TODO', 32 | id, 33 | }); 34 | }} 35 | /> 36 |
    { 39 | dispatch({ 40 | type: 'SET_VISIBILITY_FILTER', 41 | filter: newVisibility, 42 | }); 43 | }} 44 | /> 45 |
    46 | ) 47 | } 48 | 49 | export default ReduxApp; -------------------------------------------------------------------------------- /src/examples/2-data-model/1-simple-todo/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { TodoType, defaultTodos, VisibilityFilter } from './AppState'; 3 | import AddTodo from './components/AddTodo'; 4 | import TodoList from './components/TodoList'; 5 | import Footer from './components/Footer'; 6 | 7 | export default function App() { 8 | const [todos, setTodos] = useState(defaultTodos); 9 | const [visibilityFilter, setVisibilityFilter] = useState('SHOW_ALL'); 10 | return ( 11 |
    12 | { 14 | const highestID = todos.length > 0 15 | ? todos.reduce( 16 | (acc, cur) => cur.id > acc.id 17 | ? cur 18 | : acc 19 | ).id 20 | : 0; 21 | const newTodo: TodoType = { 22 | id: highestID + 1, 23 | text, 24 | completed: false, 25 | } 26 | setTodos([...todos, newTodo]); 27 | }} 28 | /> 29 | { 33 | const newTodos = todos.map( 34 | t => t.id === id 35 | ? { 36 | ...t, 37 | completed: !t.completed, 38 | } 39 | : t 40 | ); 41 | setTodos(newTodos); 42 | }} 43 | /> 44 |
    48 |
    49 | ) 50 | } 51 | 52 | -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/2-rxjs/bonus-test-router/todorouter.test.tsx: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/lib/pipeable'; 2 | import * as r from 'rxjs'; 3 | import * as ro from 'rxjs/operators'; 4 | import assert from 'assert'; 5 | import { safeReducer, curriedReducer } from './reducer'; 6 | import { defaultAppState } from '../../1-redux/1-global-state/AppState'; 7 | import { Location, format } from '../../../2-data-model/4-ssot/Location'; 8 | import { AppAction } from '../../1-redux/3-morphic-ts/AppAction'; 9 | 10 | test('New filters change the url', async () => { 11 | const urlHistoryStream = new r.Subject(); 12 | const closeStream = new r.Subject(); 13 | const urlHistory = pipe( 14 | urlHistoryStream, 15 | ro.takeUntil(closeStream), 16 | ro.toArray(), 17 | ).toPromise(); 18 | 19 | const reducer = safeReducer( 20 | curriedReducer( 21 | (a) => urlHistoryStream.next(a), 22 | ), 23 | defaultAppState, 24 | AppAction 25 | ) 26 | reducer( 27 | defaultAppState, 28 | AppAction.of.SET_VISIBILITY_FILTER({ 29 | filter: 'SHOW_COMPLETED' 30 | }) 31 | ) 32 | reducer( 33 | defaultAppState, 34 | AppAction.of.SET_VISIBILITY_FILTER({ 35 | filter: 'SHOW_ACTIVE' 36 | }) 37 | ) 38 | reducer( 39 | defaultAppState, 40 | AppAction.of.SET_VISIBILITY_FILTER({ 41 | filter: 'SHOW_ALL' 42 | }) 43 | ) 44 | 45 | closeStream.next(); 46 | await urlHistory 47 | .then(allPushedLocations => assert.deepStrictEqual( 48 | allPushedLocations, 49 | [ 50 | Location.of.Completed({ value: {} }), 51 | Location.of.Active({ value: {} }), 52 | Location.of.Landing({ value: {} }), 53 | ].map(format) 54 | )) 55 | }); -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/2-rxjs/1-currying/1-piping-currying.ts: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/lib/pipeable' 2 | 3 | export const funcApplication = Array.isArray( 4 | Array.of( 5 | String( 6 | Math.floor(1234.5678) 7 | ) 8 | ) 9 | ) 10 | 11 | const a = 1234.5678 12 | const b = Math.floor(a) 13 | const c = String(b) 14 | const d = Array.of(c) 15 | export const streamlined = Array.isArray(d) 16 | 17 | export const piped = pipe( 18 | 1234.5678, 19 | a => Math.floor(a), 20 | b => String(b), 21 | c => Array.of(c), 22 | d => Array.isArray(d), 23 | ); 24 | 25 | export const trysomething = pipe( 26 | 1234.5678, 27 | a => { 28 | const justFunc = Math.floor 29 | const retval = justFunc(a) 30 | return retval; 31 | }, 32 | b => String(b), 33 | c => Array.of(c), 34 | d => Array.isArray(d), 35 | ); 36 | 37 | export const curriedPipe = pipe( 38 | 1234.5678, 39 | Math.floor, 40 | String, 41 | Array.of, 42 | Array.isArray, 43 | ); 44 | 45 | const plus = (a: number, b: number) => a + b 46 | /* 47 | const a = 1234.5678 48 | const b = plus(28, a) 49 | const c = Math.floor(b) 50 | const d = String(c) 51 | const e = Array.of(d) 52 | export const output = Array.isArray(e) 53 | */ 54 | /* 55 | const output = pipe( 56 | 1234.5678, 57 | plus(28, _), 58 | Math.floor, 59 | String, 60 | Array.of, 61 | Array.isArray 62 | )*/ 63 | 64 | const curriedPlus = (a: number) => (b: number) => a + b 65 | /* 66 | const a = 1234.5678 67 | const b = curriedPlus(28)(a) 68 | const c = Math.floor(b) 69 | const d = String(c) 70 | const e = Array.of(d) 71 | const output = Array.isArray(e) 72 | */ 73 | export const pipedOutput = pipe( 74 | 1234.5678, 75 | curriedPlus(28), 76 | Math.floor, 77 | String, 78 | Array.of, 79 | Array.isArray 80 | ) 81 | -------------------------------------------------------------------------------- /src/examples/1-react-router/7-morphic-ts-routing/code.ts: -------------------------------------------------------------------------------- 1 | import { ADTType } from '@morphic-ts/adt'; 2 | import * as R from 'fp-ts-routing'; 3 | import { routingFromMatches4 } from 'morphic-ts-routing'; 4 | 5 | const home = R.end; 6 | const about = R.lit('about').then(R.end); 7 | const topics = R.lit('topics').then(R.end); 8 | const topicsID = R.lit('topics') 9 | .then(R.str('id')) 10 | .then(R.end); 11 | 12 | const { parse, format, adt: Location } = routingFromMatches4( 13 | ['Home', home], 14 | ['About', about], 15 | ['Topics', topics], 16 | ['TopicsID', topicsID], 17 | ); 18 | type Location = ADTType; 19 | 20 | const demo = Location.of.TopicsID({ value: { id: 'anything' } }); 21 | 22 | const urls = [ 23 | format({ type: 'Home', value: {} }), 24 | format({ type: 'About', value: {} }), 25 | format({ type: 'Topics', value: {} }), 26 | format({ type: 'TopicsID', value: { id: 'someid' } }), 27 | format({ type: 'TopicsID', value: { id: "what's percent encoding?" } }), 28 | format({ type: 'NotFound' }), 29 | ]; 30 | urls.forEach(urls => { 31 | console.log(`formatted url: ${urls}`); 32 | }); 33 | 34 | const locations = [ 35 | parse(''), 36 | parse('/'), 37 | parse('/about'), 38 | parse('/abou'), 39 | parse('about'), 40 | parse('/about/extra/segments'), 41 | parse('/topics'), 42 | parse('/topics/anything'), 43 | parse("/topics/what's%20percent%20encoding%3F"), 44 | ]; 45 | locations.forEach(location => { 46 | console.log(`parsed location type: ${JSON.stringify(location)}`); 47 | }); 48 | 49 | 50 | /* fs.writeFile(`src/RoutingFromMatches5.ts`, codegenWithNumRoutes(5), err => { 51 | // throws an error, you could also catch it here 52 | if (err) throw err; 53 | 54 | // success case, the file was saved 55 | console.log('RoutingFromMatches100.ts saved!'); 56 | }); */ -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/4-test/reducer.test.ts: -------------------------------------------------------------------------------- 1 | import { curriedReducer, safeReducer } from '../3-morphic-ts/reducer'; 2 | import { AppAction } from '../3-morphic-ts/AppAction'; 3 | import { defaultAppState, TodoType, VisibilityFilter } from '../1-global-state/AppState'; 4 | 5 | describe('Todo App Reducer', () => { 6 | 7 | const reducer = safeReducer(curriedReducer, defaultAppState, AppAction); 8 | 9 | it('Can add a todo', () => { 10 | const newTodoText = 'Hot Dog Eating Contest' 11 | const newState = reducer( 12 | defaultAppState, 13 | AppAction.of.ADD_TODO({ 14 | text: newTodoText, 15 | }) 16 | ) 17 | expect(newState.todos) 18 | .toContainEqual({ 19 | completed: false, 20 | id: 2, 21 | text: newTodoText, 22 | }); 23 | }); 24 | it('Can set the visibility filter', () => { 25 | expect(defaultAppState.visibilityFilter) 26 | .toMatch('SHOW_ALL' as VisibilityFilter) 27 | const newState = reducer( 28 | defaultAppState, 29 | AppAction.of.SET_VISIBILITY_FILTER({ 30 | filter: 'SHOW_ACTIVE' 31 | }) 32 | ) 33 | expect(newState.visibilityFilter) 34 | .toMatch('SHOW_ACTIVE' as VisibilityFilter); 35 | }); 36 | it('Can toggle a todo\'s completion', () => { 37 | const testID = 0; 38 | expect( 39 | defaultAppState 40 | .todos 41 | .find(t => t.id === testID) 42 | ?.completed 43 | ) 44 | .toBeFalsy() 45 | const newState = reducer( 46 | defaultAppState, 47 | AppAction.of.TOGGLE_TODO({ 48 | id: testID, 49 | }) 50 | ) 51 | expect( 52 | newState 53 | .todos 54 | .find(t => t.id === testID) 55 | ?.completed 56 | ) 57 | .toBeTruthy() 58 | }); 59 | }); -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/5-router-attempt/ReduxApp.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppState } from '../1-global-state/AppState'; 3 | import AddTodo from '../../../2-data-model/1-simple-todo/components/AddTodo'; 4 | import TodoList from '../../../2-data-model/1-simple-todo/components/TodoList'; 5 | import Footer from '../../../2-data-model/1-simple-todo/components/Footer'; 6 | import { useSelector, useDispatch } from 'react-redux'; 7 | import { AppAction } from '../3-morphic-ts/AppAction'; 8 | import { parse, routeToVisibilityFilter } from '../../../2-data-model/4-ssot/Location'; 9 | 10 | const ReduxApp = () => { 11 | const state = useSelector(s => s); 12 | const dispatch = useDispatch<(a: AppAction) => void>(); 13 | const { 14 | todos, 15 | visibilityFilter, 16 | } = state; 17 | window.addEventListener('popstate', () => { 18 | dispatch( 19 | AppAction.of.SET_VISIBILITY_FILTER({ 20 | filter: routeToVisibilityFilter( 21 | parse( 22 | window.location.pathname, 23 | ) 24 | ), 25 | }) 26 | ); 27 | }); 28 | return ( 29 |
    30 | { 32 | dispatch( 33 | AppAction.of.ADD_TODO({ text }) 34 | ) 35 | }} 36 | /> 37 | { 41 | dispatch( 42 | AppAction.of.TOGGLE_TODO({ id }) 43 | ); 44 | }} 45 | /> 46 |
    { 49 | dispatch( 50 | AppAction.of.SET_VISIBILITY_FILTER({ filter }) 51 | ); 52 | }} 53 | /> 54 |
    55 | ) 56 | } 57 | 58 | export default ReduxApp; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
    32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/3-redux-observable/3-test/1-pushState/todorouter.test.tsx: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/lib/pipeable'; 2 | import * as r from 'rxjs'; 3 | import * as ro from 'rxjs/operators'; 4 | import assert from 'assert'; 5 | import epic from './epic'; 6 | import { AppState } from '../../../1-redux/1-global-state/AppState'; 7 | import { Location, format } from '../../../../2-data-model/4-ssot/Location'; 8 | import { AppAction } from '../../2-pushState/AppAction'; 9 | import { StateObservable, ActionsObservable } from 'redux-observable'; 10 | 11 | test('New filters change the url', async () => { 12 | const urlHistoryStream = new r.Subject(); 13 | const closeStream = new r.Subject(); 14 | const urlHistory = pipe( 15 | urlHistoryStream, 16 | ro.takeUntil(closeStream), 17 | ro.toArray(), 18 | ).toPromise(); 19 | 20 | const action$ = pipe( 21 | [ 22 | AppAction.of.SET_VISIBILITY_FILTER_LOCATION({ 23 | filter: 'SHOW_COMPLETED' 24 | }), 25 | AppAction.of.SET_VISIBILITY_FILTER_LOCATION({ 26 | filter: 'SHOW_ACTIVE' 27 | }), 28 | AppAction.of.SET_VISIBILITY_FILTER_LOCATION({ 29 | filter: 'SHOW_ALL' 30 | }), 31 | ], 32 | arr => ActionsObservable.from(arr) 33 | ); 34 | const state$ = new StateObservable( 35 | new r.Subject(), 36 | undefined as any, 37 | ); 38 | const appEpic = epic( 39 | url => urlHistoryStream.next(url), 40 | r.empty(), 41 | () => '/' 42 | ); 43 | 44 | await appEpic(action$, state$, null).toPromise(); 45 | 46 | closeStream.next(); 47 | await urlHistory 48 | .then(allPushedLocations => assert.deepStrictEqual( 49 | allPushedLocations, 50 | [ 51 | Location.of.Completed({ value: {} }), 52 | Location.of.Active({ value: {} }), 53 | Location.of.Landing({ value: {} }), 54 | ].map(format) 55 | )) 56 | }); -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/bonus-monocle-ts/reducer.ts: -------------------------------------------------------------------------------- 1 | import { flow } from 'fp-ts/lib/function'; 2 | import { pipe } from 'fp-ts/lib/pipeable'; 3 | import { ordNumber } from 'fp-ts/lib/Ord'; 4 | import { getJoinSemigroup } from 'fp-ts/lib/Semigroup'; 5 | import * as O from 'fp-ts/lib/Option'; 6 | import * as NEA from 'fp-ts/lib/NonEmptyArray'; 7 | import * as A from 'fp-ts/lib/Array'; 8 | import * as M from 'monocle-ts'; 9 | import { Reducer, Action } from "redux"; 10 | import { AppState, TodoType } from "../1-global-state/AppState"; 11 | import { AppAction } from "../3-morphic-ts/AppAction"; 12 | import { ADT } from '@morphic-ts/adt'; 13 | 14 | export const safeReducer = ( 15 | curriedReducer: (a: A) => (s: S) => S, 16 | defaultState: S, 17 | adt: ADT 18 | ): Reducer => ( 19 | nullableState, 20 | action 21 | ) => { 22 | if (!nullableState || !adt.verified(action)) return defaultState; 23 | return curriedReducer(action)(nullableState); 24 | } 25 | 26 | export const curriedReducer = AppAction.matchStrict<(a: AppState) => AppState>({ 27 | ADD_TODO: ({ text }) => M.Lens 28 | .fromProp()('todos') 29 | .modify(ts => pipe( 30 | ts, 31 | NEA.fromArray, 32 | O.map(flow( 33 | NEA.map(t => t.id), 34 | NEA.fold(getJoinSemigroup(ordNumber)), 35 | highestID => highestID + 1, 36 | )), 37 | O.getOrElse(() => 0), 38 | id => A.snoc( 39 | ts, 40 | { id, text, completed: false } 41 | ), 42 | )), 43 | SET_VISIBILITY_FILTER: ({ filter }) => M.Lens 44 | .fromProp()('visibilityFilter') 45 | .set(filter), 46 | TOGGLE_TODO: ({ id }) => M.Lens 47 | .fromProp()('todos') 48 | .composeTraversal(M.fromTraversable(A.array)()) 49 | .filter(t => t.id === id) 50 | .composeLens(M.Lens.fromProp()('completed')) 51 | .modify(completed => !completed), 52 | }); 53 | -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/1-global-state/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { TodoType, defaultAppState, AppState } from './AppState'; 3 | import AddTodo from '../../../2-data-model/1-simple-todo/components/AddTodo'; 4 | import TodoList from '../../../2-data-model/1-simple-todo/components/TodoList'; 5 | import Footer from '../../../2-data-model/1-simple-todo/components/Footer'; 6 | 7 | export default function App() { 8 | const [state, setState] = useState(defaultAppState); 9 | const { 10 | todos, 11 | visibilityFilter, 12 | } = state; 13 | return ( 14 |
    15 | { 17 | const highestID = todos.length > 0 18 | ? todos.reduce( 19 | (acc, cur) => cur.id > acc.id 20 | ? cur 21 | : acc 22 | ).id 23 | : 0; 24 | const newTodo: TodoType = { 25 | id: highestID + 1, 26 | text, 27 | completed: false, 28 | } 29 | setState({ 30 | ...state, 31 | todos: [...todos, newTodo] 32 | }); 33 | }} 34 | /> 35 | { 39 | const newTodos = todos.map( 40 | t => t.id === id 41 | ? { 42 | ...t, 43 | completed: !t.completed, 44 | } 45 | : t 46 | ); 47 | setState({ 48 | ...state, 49 | todos: newTodos, 50 | }); 51 | }} 52 | /> 53 |
    { 56 | setState({ 57 | ...state, 58 | visibilityFilter: newVisibility, 59 | }); 60 | }} 61 | /> 62 |
    63 | ) 64 | } 65 | 66 | -------------------------------------------------------------------------------- /src/examples/1-react-router/6-morphic-ts/code.ts: -------------------------------------------------------------------------------- 1 | import { makeADT, ofType, ADTType } from '@morphic-ts/adt'; 2 | import * as R from 'fp-ts-routing'; 3 | 4 | const Location = makeADT('type')({ 5 | Home: ofType(), 6 | About: ofType(), 7 | Topics: ofType(), 8 | TopicsID: ofType<{ type: 'TopicsID'; id: string }>(), 9 | NotFound: ofType(), 10 | }); 11 | type Location = ADTType; 12 | 13 | const home = R.end; 14 | const about = R.lit('about').then(R.end); 15 | const topics = R.lit('topics').then(R.end); 16 | const topicsID = R.lit('topics') 17 | .then(R.str('id')) 18 | .then(R.end); 19 | 20 | const router = R.zero() 21 | .alt( 22 | home.parser.map(() => Location.of.Home({})), 23 | ) 24 | .alt(about.parser.map(() => Location.of.About({}))) 25 | .alt(topics.parser.map(() => Location.of.Topics({}))) 26 | .alt(topicsID.parser.map(({ id }) => Location.of.TopicsID({ id }))); 27 | 28 | const parser = (s: string): Location => 29 | R.parse(router, R.Route.parse(s), { type: 'NotFound' }); 30 | 31 | const formatter = Location.matchStrict({ 32 | Home: l => R.format(home.formatter, l), 33 | About: l => R.format(about.formatter, l), 34 | Topics: l => R.format(topics.formatter, l), 35 | TopicsID: l => R.format(topicsID.formatter, l), 36 | NotFound: () => '/', 37 | }); 38 | 39 | const locations = [ 40 | parser(''), 41 | parser('/'), 42 | parser('/home'), 43 | parser('home/'), 44 | parser('/users/components'), 45 | parser('/users/props-v-state'), 46 | parser('/users/two/'), 47 | parser('/users/'), 48 | parser('/users'), 49 | ]; 50 | locations.map(location => { 51 | console.log(`parsed location type: ${JSON.stringify(location)}`); 52 | }); 53 | 54 | const urls = [ 55 | formatter({ type: 'Home' }), 56 | formatter({ type: 'About' }), 57 | formatter({ type: 'Topics' }), 58 | formatter({ type: 'TopicsID', id: 'someid' }), 59 | formatter({ type: 'Home' }), 60 | ]; 61 | urls.map(urls => { 62 | console.log(`formatted url: ${urls}`); 63 | }); -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/2-rxjs/bonus-test-router/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Reducer, Action } from "redux"; 2 | import { AppState, TodoType } from "../../1-redux/1-global-state/AppState"; 3 | import { AppAction } from '../../1-redux/3-morphic-ts/AppAction'; 4 | import { ADT } from "@morphic-ts/adt"; 5 | import { format, visibilityFilterToRoute } from '../../../2-data-model/4-ssot/Location'; 6 | 7 | export const safeReducer =
    ( 8 | curriedReducer: (a: A) => (s: S) => S, 9 | defaultState: S, 10 | adt: ADT 11 | ): Reducer => ( 12 | nullableState, 13 | action 14 | ) => { 15 | if (!nullableState || !adt.verified(action)) return defaultState; 16 | return curriedReducer(action)(nullableState); 17 | } 18 | 19 | export const curriedReducer = ( 20 | pushUrl: (url: string) => void, 21 | ) => AppAction.matchStrict<(a: AppState) => AppState>({ 22 | ADD_TODO: ({ text }) => (state) => { 23 | const { todos } = state; 24 | const highestID = todos.length > 0 25 | ? todos.reduce( 26 | (acc, cur) => cur.id > acc.id 27 | ? cur 28 | : acc 29 | ).id 30 | : 0; 31 | const newTodo: TodoType = { 32 | id: highestID + 1, 33 | text: text, 34 | completed: false, 35 | } 36 | return { 37 | ...state, 38 | todos: [...todos, newTodo] 39 | }; 40 | }, 41 | SET_VISIBILITY_FILTER: ({ filter }) => (state) => { 42 | pushUrl(format( 43 | visibilityFilterToRoute( 44 | filter 45 | ) 46 | )) 47 | return { 48 | ...state, 49 | visibilityFilter: filter, 50 | }; 51 | }, 52 | TOGGLE_TODO: ({ id }) => (state) => { 53 | const { todos } = state; 54 | const newTodos = todos.map( 55 | t => t.id === id 56 | ? { 57 | ...t, 58 | completed: !t.completed, 59 | } 60 | : t 61 | ); 62 | return { 63 | ...state, 64 | todos: newTodos, 65 | }; 66 | }, 67 | }); -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/5-router-attempt/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Reducer, Action } from "redux"; 2 | import { AppState, TodoType } from "../1-global-state/AppState"; 3 | import { AppAction } from '../3-morphic-ts/AppAction'; 4 | import { ADT } from "@morphic-ts/adt"; 5 | import { format, visibilityFilterToRoute } from '../../../2-data-model/4-ssot/Location'; 6 | 7 | export const safeReducer = ( 8 | curriedReducer: (a: A) => (s: S) => S, 9 | defaultState: S, 10 | adt: ADT 11 | ): Reducer => ( 12 | nullableState, 13 | action 14 | ) => { 15 | if (!nullableState || !adt.verified(action)) return defaultState; 16 | return curriedReducer(action)(nullableState); 17 | } 18 | 19 | export const curriedReducer = AppAction.matchStrict<(a: AppState) => AppState>({ 20 | ADD_TODO: ({ text }) => (state) => { 21 | const { todos } = state; 22 | const highestID = todos.length > 0 23 | ? todos.reduce( 24 | (acc, cur) => cur.id > acc.id 25 | ? cur 26 | : acc 27 | ).id 28 | : 0; 29 | const newTodo: TodoType = { 30 | id: highestID + 1, 31 | text: text, 32 | completed: false, 33 | } 34 | return { 35 | ...state, 36 | todos: [...todos, newTodo] 37 | }; 38 | }, 39 | SET_VISIBILITY_FILTER: ({ filter }) => (state) => { 40 | window.history.pushState( 41 | null, 42 | '', 43 | format( 44 | visibilityFilterToRoute( 45 | filter 46 | ) 47 | ), 48 | ) 49 | return { 50 | ...state, 51 | visibilityFilter: filter, 52 | }; 53 | }, 54 | TOGGLE_TODO: ({ id }) => (state) => { 55 | const { todos } = state; 56 | const newTodos = todos.map( 57 | t => t.id === id 58 | ? { 59 | ...t, 60 | completed: !t.completed, 61 | } 62 | : t 63 | ); 64 | return { 65 | ...state, 66 | todos: newTodos, 67 | }; 68 | }, 69 | }); -------------------------------------------------------------------------------- /src/examples/1-react-router/2-pushstate-onpopstate/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Link from '../common/Link'; 3 | 4 | export default function App() { 5 | const [counter, setCounter] = useState(0); 6 | const [pathname, setPathname] = useState(window.location.pathname); 7 | const updatePathname = (newLocation: string) => { 8 | setPathname(newLocation); 9 | window.history.pushState(null, '', newLocation); 10 | } 11 | window.addEventListener('popstate', () => { 12 | setPathname(window.location.pathname); 13 | }); 14 | let innerComponent: JSX.Element; 15 | switch (pathname) { 16 | case '/': 17 | innerComponent = ; 18 | break; 19 | case '/about': 20 | innerComponent = ; 21 | break; 22 | case '/users': 23 | innerComponent = ; 24 | break; 25 | default: 26 | innerComponent = ; 27 | break; 28 | } 29 | return ( 30 |
    31 | 36 | 64 | {innerComponent} 65 |
    66 | ); 67 | } 68 | 69 | function Home() { 70 | return

    Home

    ; 71 | } 72 | 73 | function About() { 74 | return

    About

    ; 75 | } 76 | 77 | function Users() { 78 | return

    Users

    ; 79 | } 80 | 81 | 82 | -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/1-redux/3-morphic-ts/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Reducer, Action } from "redux"; 2 | import { AppState, TodoType } from "../1-global-state/AppState"; 3 | import { AppAction } from "./AppAction"; 4 | import { ADT } from "@morphic-ts/adt"; 5 | 6 | /* const safeReducer = ( 7 | curriedReducer: (a: AppAction) => (s: AppState) => AppState, 8 | ): Reducer => ( 9 | nullableState, 10 | action 11 | ) => { 12 | if (!nullableState || !AppAction.verified(action)) return defaultAppState; 13 | return curriedReducer(action)(nullableState); 14 | } */ 15 | 16 | export const safeReducer =
    ( 17 | curriedReducer: (a: A) => (s: S) => S, 18 | defaultState: S, 19 | adt: ADT 20 | ): Reducer => ( 21 | nullableState, 22 | action 23 | ) => { 24 | if (!nullableState || !adt.verified(action)) return defaultState; 25 | return curriedReducer(action)(nullableState); 26 | } 27 | 28 | export const curriedReducer = AppAction.matchStrict<(a: AppState) => AppState>({ 29 | ADD_TODO: ({ text }) => (state) => { 30 | const { todos } = state; 31 | const highestID = todos.length > 0 32 | ? todos.reduce( 33 | (acc, cur) => cur.id > acc.id 34 | ? cur 35 | : acc 36 | ).id 37 | : 0; 38 | const newTodo: TodoType = { 39 | id: highestID + 1, 40 | text: text, 41 | completed: false, 42 | } 43 | return { 44 | ...state, 45 | todos: [...todos, newTodo] 46 | }; 47 | }, 48 | SET_VISIBILITY_FILTER: ({ filter }) => (state) => { 49 | return { 50 | ...state, 51 | visibilityFilter: filter, 52 | }; 53 | }, 54 | TOGGLE_TODO: ({ id }) => (state) => { 55 | const { todos } = state; 56 | const newTodos = todos.map( 57 | t => t.id === id 58 | ? { 59 | ...t, 60 | completed: !t.completed, 61 | } 62 | : t 63 | ); 64 | return { 65 | ...state, 66 | todos: newTodos, 67 | }; 68 | }, 69 | }); -------------------------------------------------------------------------------- /src/examples/optional-next-js/1-router/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Link from '../../1-react-router/common/Link'; 3 | import window from './NodeSafeWindow'; 4 | 5 | export default function App() { 6 | const [counter, setCounter] = useState(0); 7 | const [pathname, setPathname] = useState(window.location.pathname); 8 | const updatePathname = (newLocation: string) => { 9 | setPathname(newLocation); 10 | window.history.pushState(null, '', newLocation); 11 | } 12 | window.addEventListener('popstate', () => { 13 | setPathname(window.location.pathname); 14 | }) 15 | let innerComponent: JSX.Element; 16 | switch (pathname) { 17 | case '/': 18 | innerComponent = ; 19 | break; 20 | case '/about': 21 | innerComponent = ; 22 | break; 23 | case '/users': 24 | innerComponent = ; 25 | break; 26 | default: 27 | innerComponent =
    ; 28 | break; 29 | } 30 | return ( 31 |
    32 | 37 | 65 | {innerComponent} 66 |
    67 | ); 68 | } 69 | 70 | function Home() { 71 | return

    Home

    ; 72 | } 73 | 74 | function About() { 75 | return

    About

    ; 76 | } 77 | 78 | function Users() { 79 | return

    Users

    ; 80 | } 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/examples/1-react-router/2-bonus-hash-history-state/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Link from '../common/Link'; 3 | 4 | export default function App() { 5 | const [counter, setCounter] = useState(0); 6 | const [pathname, setPathname] = useState(window.location.pathname); 7 | const updatePathname = (newurl: string) => { 8 | setPathname(newurl); 9 | window.history.pushState(counter, '', newurl); 10 | // window.history.replaceState(counter, '', newurl); 11 | } 12 | window.addEventListener('popstate', ev => { 13 | setCounter(ev.state as number) 14 | setPathname(window.location.pathname); 15 | }); 16 | let innerComponent: JSX.Element; 17 | switch (pathname) { 18 | case '/': 19 | innerComponent = ; 20 | break; 21 | case '/about': 22 | innerComponent = ; 23 | break; 24 | case '/users': 25 | innerComponent = ; 26 | break; 27 | default: 28 | innerComponent = ; 29 | break; 30 | } 31 | return ( 32 |
    33 | 38 | 66 | {innerComponent} 67 |
    68 | ); 69 | } 70 | 71 | function Home() { 72 | return

    Home

    ; 73 | } 74 | 75 | function About() { 76 | return

    About

    ; 77 | } 78 | 79 | function Users() { 80 | return

    Users

    ; 81 | } 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/examples/1-react-router/3-union-types/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import PathnameLink from './PathnameLink'; 3 | 4 | export type Pathname = '/' | '/about' | '/users' 5 | 6 | export default function App() { 7 | const [counter, setCounter] = useState(0); 8 | const [pathname, setPathname] = useState(window.location.pathname); 9 | const updatePathname = (newurl: string) => { 10 | setPathname(newurl); 11 | window.history.pushState(null, '', newurl); 12 | } 13 | window.addEventListener('popstate', () => { 14 | setPathname(window.location.pathname); 15 | }); 16 | let innerComponent: JSX.Element; 17 | switch(pathname as Pathname) { 18 | case '/': 19 | innerComponent = ; 20 | break; 21 | case '/about': 22 | innerComponent = ; 23 | break; 24 | case '/users': 25 | innerComponent = ; 26 | break; 27 | default: 28 | innerComponent = ; 29 | break; 30 | } 31 | return ( 32 |
    33 | 38 | 66 | {innerComponent} 67 |
    68 | ); 69 | } 70 | 71 | function Home() { 72 | return

    Home

    ; 73 | } 74 | 75 | function About() { 76 | return

    About

    ; 77 | } 78 | 79 | function Users() { 80 | return

    Users

    ; 81 | } 82 | -------------------------------------------------------------------------------- /src/examples/optional-next-js/2-flicker/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Link from '../../1-react-router/common/Link'; 3 | import window from '../1-router/NodeSafeWindow'; 4 | 5 | export default function App({ 6 | initialUrl, 7 | }: { 8 | initialUrl: string; 9 | }) { 10 | const [counter, setCounter] = useState(0); 11 | const [pathname, setPathname] = useState(initialUrl); 12 | const updatePathname = (newLocation: string) => { 13 | setPathname(newLocation); 14 | window.history.pushState(null, '', newLocation); 15 | } 16 | window.addEventListener('popstate', () => { 17 | setPathname(window.location.pathname); 18 | }) 19 | let innerComponent: JSX.Element; 20 | switch (pathname) { 21 | case '/': 22 | innerComponent = ; 23 | break; 24 | case '/about': 25 | innerComponent = ; 26 | break; 27 | case '/users': 28 | innerComponent = ; 29 | break; 30 | default: 31 | innerComponent =
    ; 32 | break; 33 | } 34 | return ( 35 |
    36 | 41 | 69 | {innerComponent} 70 |
    71 | ); 72 | } 73 | 74 | function Home() { 75 | return

    Home

    ; 76 | } 77 | 78 | function About() { 79 | return

    About

    ; 80 | } 81 | 82 | function Users() { 83 | return

    Users

    ; 84 | } 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/3-redux-observable/2-pushState/reducer.ts: -------------------------------------------------------------------------------- 1 | import { identity } from 'fp-ts/lib/function'; 2 | import { pipe } from 'fp-ts/lib/pipeable'; 3 | import * as O from 'fp-ts/lib/Option'; 4 | import { Reducer, Action } from "redux"; 5 | import { AppState, TodoType } from "../../1-redux/1-global-state/AppState"; 6 | import { AppAction, TransformAction } from './AppAction'; 7 | import { ADT } from "@morphic-ts/adt"; 8 | 9 | export const safeReducer =
    ( 10 | curriedReducer: (a: A) => (s: S) => S, 11 | defaultState: S, 12 | adt: ADT 13 | ): Reducer => ( 14 | nullableState, 15 | action 16 | ) => { 17 | if (!nullableState || !adt.verified(action)) return defaultState; 18 | return curriedReducer(action)(nullableState); 19 | } 20 | 21 | export const curriedReducer = ( 22 | a: AppAction, 23 | ) => pipe( 24 | a as TransformAction, 25 | O.fromPredicate(TransformAction.verified), 26 | O.map(TransformAction.matchStrict<(a: AppState) => AppState>({ 27 | ADD_TODO: ({ text }) => (state) => { 28 | const { todos } = state; 29 | const highestID = todos.length > 0 30 | ? todos.reduce( 31 | (acc, cur) => cur.id > acc.id 32 | ? cur 33 | : acc 34 | ).id 35 | : 0; 36 | const newTodo: TodoType = { 37 | id: highestID + 1, 38 | text: text, 39 | completed: false, 40 | } 41 | return { 42 | ...state, 43 | todos: [...todos, newTodo] 44 | }; 45 | }, 46 | SET_VISIBILITY_FILTER: ({ filter }) => (state) => { 47 | return { 48 | ...state, 49 | visibilityFilter: filter, 50 | }; 51 | }, 52 | TOGGLE_TODO: ({ id }) => (state) => { 53 | const { todos } = state; 54 | const newTodos = todos.map( 55 | t => t.id === id 56 | ? { 57 | ...t, 58 | completed: !t.completed, 59 | } 60 | : t 61 | ); 62 | return { 63 | ...state, 64 | todos: newTodos, 65 | }; 66 | }, 67 | })), 68 | O.getOrElse((): (s: AppState) => AppState => identity), 69 | ); 70 | -------------------------------------------------------------------------------- /src/examples/optional-next-js/4-data-fetch/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Link from '../../1-react-router/common/Link'; 3 | import window from '../1-router/NodeSafeWindow'; 4 | 5 | export default function App({ 6 | initialUrl, 7 | fetched, 8 | }: { 9 | initialUrl: string; 10 | fetched: string; 11 | }) { 12 | const [counter, setCounter] = useState(0); 13 | const [pathname, setPathname] = useState(initialUrl); 14 | const updatePathname = (newLocation: string) => { 15 | setPathname(newLocation); 16 | window.history.pushState(null, '', newLocation); 17 | } 18 | window.addEventListener('popstate', () => { 19 | setPathname(window.location.pathname); 20 | }) 21 | let innerComponent: JSX.Element; 22 | switch (pathname) { 23 | case '/': 24 | innerComponent = ; 25 | break; 26 | case '/about': 27 | innerComponent = ; 28 | break; 29 | case '/users': 30 | innerComponent = ; 31 | break; 32 | default: 33 | innerComponent =
    ; 34 | break; 35 | } 36 | return ( 37 |
    38 | {fetched} 39 | 44 | 72 | {innerComponent} 73 |
    74 | ); 75 | } 76 | 77 | function Home() { 78 | return

    Home

    ; 79 | } 80 | 81 | function About() { 82 | return

    About

    ; 83 | } 84 | 85 | function Users() { 86 | return

    Users

    ; 87 | } 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/examples/optional-next-js/5-hydration-safe-fetch/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Link from '../../1-react-router/common/Link'; 3 | import window from './NodeSafeWindow'; 4 | 5 | export default function App({ 6 | initialUrl, 7 | }: { 8 | initialUrl: string; 9 | }) { 10 | const fetched = window.__INITIAL__DATA__ 11 | const [counter, setCounter] = useState(0); 12 | const [pathname, setPathname] = useState(initialUrl); 13 | const updatePathname = (newLocation: string) => { 14 | setPathname(newLocation); 15 | window.history.pushState(null, '', newLocation); 16 | } 17 | window.addEventListener('popstate', () => { 18 | setPathname(window.location.pathname); 19 | }) 20 | let innerComponent: JSX.Element; 21 | switch (pathname) { 22 | case '/': 23 | innerComponent = ; 24 | break; 25 | case '/about': 26 | innerComponent = ; 27 | break; 28 | case '/users': 29 | innerComponent = ; 30 | break; 31 | default: 32 | innerComponent =
    ; 33 | break; 34 | } 35 | return ( 36 |
    37 | {fetched} 38 | 43 | 71 | {innerComponent} 72 |
    73 | ); 74 | } 75 | 76 | function Home() { 77 | return

    Home

    ; 78 | } 79 | 80 | function About() { 81 | return

    About

    ; 82 | } 83 | 84 | function Users() { 85 | return

    Users

    ; 86 | } 87 | 88 | 89 | -------------------------------------------------------------------------------- /src/examples/1-react-router/5-bonus-ints-queries-types/code.ts: -------------------------------------------------------------------------------- 1 | import * as t from 'io-ts' 2 | import { lit, str, query, Route, parse, format } from 'fp-ts-routing' 3 | 4 | const AorBCodec = t.union([ 5 | t.literal('a'), 6 | t.literal('b') 7 | ]) 8 | type AorB = t.TypeOf 9 | 10 | interface AccountQuery { 11 | type: 'AccountQuery'; 12 | accountId: string; // accountId: number //accountId: AorB 13 | pathparam: string; 14 | } 15 | interface NotFound { 16 | readonly type: 'NotFound' 17 | } 18 | type Location = AccountQuery | NotFound 19 | 20 | const accountMatch = lit('accounts') 21 | .then(str('accountId')) //.then(int('accountId')) //.then(type('accountId', AorBCodec)) 22 | .then(lit('files')) 23 | .then(query( 24 | t.strict({ pathparam: t.string }) 25 | )) 26 | 27 | const router = accountMatch.parser.map( 28 | ({ accountId, pathparam }) => ({ 29 | type: 'AccountQuery', 30 | accountId, 31 | pathparam, 32 | }) 33 | ); 34 | 35 | const parser = (s: string): Location => parse( 36 | router, 37 | Route.parse(s), 38 | { type: 'NotFound' }, 39 | ) 40 | const formatter = (l: Location): string => { 41 | switch (l.type) { 42 | case 'AccountQuery': 43 | return format(accountMatch.formatter, l); 44 | case 'NotFound': 45 | return '/'; 46 | } 47 | } 48 | 49 | const locations = [ 50 | parser('/someaccountid/files?pathparam=somepath'), 51 | parser('/someaccountid/notfiles?pathparam=somepath'), 52 | parser('/someaccountid/files?notpathparam=somepath'), 53 | // parser('/a/files?pathparam=somepath'), 54 | // parser('/b/files?pathparam=somepath'), 55 | // parser('/c/files?pathparam=somepath'), 56 | ] 57 | locations.forEach(location => { 58 | console.log(`parsed location type: ${JSON.stringify(location)}`); 59 | }); 60 | 61 | const urls = [ 62 | formatter({ 63 | type: 'AccountQuery', 64 | accountId: 'someaccountid', 65 | pathparam: 'somepath', 66 | }), 67 | /* formatter({ 68 | type: 'AccountQuery', 69 | accountId: 'c', 70 | pathparam: 'somepath', 71 | }), */ 72 | /* formatter({ 73 | type: 'AccountQuery', 74 | accountId: 'a', 75 | pathparam: 'somepath', 76 | }), */ 77 | ] 78 | urls.forEach(urls => { 79 | console.log(`formatted url: ${urls}`); 80 | }); 81 | 82 | console.log() -------------------------------------------------------------------------------- /src/examples/1-react-router/6-bonus-other-adts/CodegenLocation.ts: -------------------------------------------------------------------------------- 1 | import { Eq, fromEquals } from "fp-ts/lib/Eq"; 2 | import { Prism } from "monocle-ts"; 3 | 4 | export type Location = { 5 | readonly type: "Home"; 6 | } | { 7 | readonly type: "About"; 8 | } | { 9 | readonly type: "Topics"; 10 | } | { 11 | readonly type: "TopicsID"; 12 | readonly value0: string; 13 | } | { 14 | readonly type: "NotFound"; 15 | }; 16 | 17 | export const home: Location = { type: "Home" }; 18 | 19 | export const about: Location = { type: "About" }; 20 | 21 | export const topics: Location = { type: "Topics" }; 22 | 23 | export function topicsID(value0: string): Location { return { type: "TopicsID", value0 }; } 24 | 25 | export const notFound: Location = { type: "NotFound" }; 26 | 27 | export function fold(handlers: { 28 | onHome: () => R; 29 | onAbout: () => R; 30 | onTopics: () => R; 31 | onTopicsID: (value0: string) => R; 32 | onNotFound: () => R; 33 | }): (fa: Location) => R { return fa => { switch (fa.type) { 34 | case "Home": return handlers.onHome(); 35 | case "About": return handlers.onAbout(); 36 | case "Topics": return handlers.onTopics(); 37 | case "TopicsID": return handlers.onTopicsID(fa.value0); 38 | case "NotFound": return handlers.onNotFound(); 39 | } }; } 40 | 41 | export const _Home: Prism = Prism.fromPredicate(s => s.type === "Home"); 42 | 43 | export const _About: Prism = Prism.fromPredicate(s => s.type === "About"); 44 | 45 | export const _Topics: Prism = Prism.fromPredicate(s => s.type === "Topics"); 46 | 47 | export const _TopicsID: Prism = Prism.fromPredicate(s => s.type === "TopicsID"); 48 | 49 | export const _NotFound: Prism = Prism.fromPredicate(s => s.type === "NotFound"); 50 | 51 | export function getEq(eqTopicsIDValue0: Eq): Eq { return fromEquals((x, y) => { if (x.type === "Home" && y.type === "Home") { 52 | return true; 53 | } if (x.type === "About" && y.type === "About") { 54 | return true; 55 | } if (x.type === "Topics" && y.type === "Topics") { 56 | return true; 57 | } if (x.type === "TopicsID" && y.type === "TopicsID") { 58 | return eqTopicsIDValue0.equals(x.value0, y.value0); 59 | } if (x.type === "NotFound" && y.type === "NotFound") { 60 | return true; 61 | } return false; }); } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Examples 3 | 1. React Router 4 | 1. Location & History APIs 5 | 0. React Router 6 | 1. pathname, anchor 7 | 2. pushState, onpopstate 8 | - Bonus - What's history state? 9 | 3. Union types 10 | 4. Tagged Unions 11 | 5. fp-ts-routing 12 | - Bonus: Ints, Queries and Types 13 | 6. morphic-ts 14 | - Bonus: verified 15 | - Bonus: other sum type libraries 16 | 7. morphic-ts-routing 17 | - (Optional) Next.js & SSR 18 | 1. Router 19 | 2. Removing flicker 20 | 3. Hyradtion-safe URL 21 | 4. Data fetch 22 | 5. Hydration-safe Data Fetch 23 | 2. Data Model 24 | 1. TodoMVC 25 | 2. react-testing-library 26 | 3. router 27 | 4. SSOT 28 | 5. test 29 | 3. Reactive Architecture with Redux 30 | 1. redux 31 | 1. global state 32 | 2. store and reducer 33 | 3. morphic-ts 34 | 4. test 35 | 5. router attempt (failure) 36 | 6. Bonus: monocle-ts 37 | 2. rxjs 38 | 1. currying, pipe & flow 39 | 2. rxjs 40 | 3. Bonus: testing the router with rxjs 41 | 3. redux-observable 42 | 1. onpopstate 43 | 2. pushState 44 | 3. Test the epic 45 | 4. TodoMVC in purer reactive architectures 46 | 1. [Elm](https://github.com/kadikraman/elm-todo/blob/master/src/Main.elm) 47 | 2. [Reflex](https://github.com/reflex-frp/reflex-todomvc/blob/develop/src/Reflex/TodoMVC.hs) 48 | 3. [Halogen](https://github.com/holdenlee/halogen-todo/blob/master/src/Main.purs) 49 | 50 | ## Running the Example Code 51 | 52 | You can run the code in this repository by pointing the `App` component import in `src/index.ts` to the `App.ts` file corresponding to the video. 53 | 54 | For the nextjs/ssr tutorial, you can run the code in this repository by pointing the `handleRoute` function import in `server/server.tsx` to the `handleRoute.ts` file corresponding to the video, as well as the `clientAppElement` function import in `server/hydrate.tsx`. 55 | 56 | ## SSR Setup 57 | 58 | [Express SSR (using create-react-app)](https://gist.github.com/anthonyjoeseph/bdcf9be5cfc515cad334b687237c1556) 59 | 60 | ### This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 61 | -------------------------------------------------------------------------------- /src/examples/2-data-model/4-ssot/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { TodoType, defaultTodos, VisibilityFilter } from '../1-simple-todo/AppState'; 3 | import AddTodo from '../1-simple-todo/components/AddTodo'; 4 | import TodoList from '../1-simple-todo/components/TodoList'; 5 | import Footer from '../1-simple-todo/components/Footer'; 6 | import { routeToVisibilityFilter, visibilityFilterToRoute, parse, format } from './Location'; 7 | 8 | export default function App() { 9 | const [todos, setTodos] = useState(defaultTodos); 10 | const [visibilityFilter, setVisibilityFilter] = useState( 11 | routeToVisibilityFilter( 12 | parse( 13 | window.location.pathname 14 | ) 15 | ), 16 | ); 17 | window.addEventListener('popstate', () => { 18 | setVisibilityFilter( 19 | routeToVisibilityFilter( 20 | parse( 21 | window.location.pathname, 22 | ) 23 | ) 24 | ) 25 | }); 26 | const updateVisibilityFilter = (newVisibility: VisibilityFilter) => { 27 | window.history.pushState( 28 | null, 29 | '', 30 | format( 31 | visibilityFilterToRoute( 32 | newVisibility 33 | ) 34 | ) 35 | ) 36 | setVisibilityFilter(newVisibility) 37 | } 38 | return ( 39 |
    40 | { 42 | const highestID = todos.length > 0 43 | ? todos.reduce( 44 | (acc, cur) => cur.id > acc.id 45 | ? cur 46 | : acc 47 | ).id 48 | : 0; 49 | const newTodo: TodoType = { 50 | id: highestID + 1, 51 | text, 52 | completed: false, 53 | } 54 | setTodos([...todos, newTodo]); 55 | }} 56 | /> 57 | { 61 | const newTodos = todos.map( 62 | t => t.id === id 63 | ? { 64 | ...t, 65 | completed: !t.completed, 66 | } 67 | : t 68 | ); 69 | setTodos(newTodos); 70 | }} 71 | /> 72 |
    76 |
    77 | ) 78 | } 79 | 80 | -------------------------------------------------------------------------------- /src/examples/2-data-model/5-test/MockableApp.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { TodoType, defaultTodos, VisibilityFilter } from '../1-simple-todo/AppState'; 3 | import AddTodo from '../1-simple-todo/components/AddTodo'; 4 | import TodoList from '../1-simple-todo/components/TodoList'; 5 | import Footer from '../1-simple-todo/components/Footer'; 6 | import { routeToVisibilityFilter, visibilityFilterToRoute, parse, format } from '../4-ssot/Location'; 7 | 8 | const MockableApp = ({ 9 | pushUrl, 10 | }: { 11 | pushUrl: (url: string) => void, 12 | }) => { 13 | const [todos, setTodos] = useState(defaultTodos); 14 | const [visibilityFilter, setVisibilityFilter] = useState( 15 | routeToVisibilityFilter( 16 | parse( 17 | window.location.pathname 18 | ) 19 | ), 20 | ); 21 | window.addEventListener('popstate', () => { 22 | setVisibilityFilter( 23 | routeToVisibilityFilter( 24 | parse( 25 | window.location.pathname, 26 | ) 27 | ) 28 | ) 29 | }); 30 | const updateVisibilityFilter = (newVisibility: VisibilityFilter) => { 31 | pushUrl( 32 | format( 33 | visibilityFilterToRoute( 34 | newVisibility 35 | ) 36 | ) 37 | ) 38 | setVisibilityFilter(newVisibility) 39 | } 40 | return ( 41 |
    42 | { 44 | const highestID = todos.length > 0 45 | ? todos.reduce( 46 | (acc, cur) => cur.id > acc.id 47 | ? cur 48 | : acc 49 | ).id 50 | : 0; 51 | const newTodo: TodoType = { 52 | id: highestID + 1, 53 | text, 54 | completed: false, 55 | } 56 | setTodos([...todos, newTodo]); 57 | }} 58 | /> 59 | { 63 | const newTodos = todos.map( 64 | t => t.id === id 65 | ? { 66 | ...t, 67 | completed: !t.completed, 68 | } 69 | : t 70 | ); 71 | setTodos(newTodos); 72 | }} 73 | /> 74 |
    78 |
    79 | ) 80 | } 81 | 82 | export default MockableApp; -------------------------------------------------------------------------------- /src/examples/1-react-router/5-fp-ts-routing/code.ts: -------------------------------------------------------------------------------- 1 | import * as R from 'fp-ts-routing'; 2 | 3 | interface Home { 4 | readonly type: 'Home'; 5 | } 6 | 7 | interface About { 8 | readonly type: 'About'; 9 | } 10 | 11 | interface Topics { 12 | readonly type: 'Topics'; 13 | } 14 | 15 | interface TopicsID { 16 | readonly type: 'TopicsID'; 17 | readonly id: string; 18 | } 19 | 20 | interface NotFound { 21 | readonly type: 'NotFound'; 22 | } 23 | 24 | type Location = Home | About | Topics | TopicsID | NotFound; 25 | 26 | const home = R.end; 27 | const about = R.lit('about').then(R.end); 28 | const topics = R.lit('topics').then(R.end); 29 | const topicsID = R.lit('topics') 30 | .then(R.str('id')) 31 | .then(R.end); 32 | 33 | const router = topicsID.parser 34 | .map(obj => { 35 | return { 36 | type: 'TopicsID', 37 | id: obj.id, 38 | }; 39 | }) 40 | .alt( 41 | home.parser.map(() => { 42 | return { 43 | type: 'Home', 44 | }; 45 | }), 46 | ) 47 | .alt( 48 | about.parser.map(() => ({ 49 | type: 'About', 50 | })), 51 | ) 52 | .alt( 53 | topics.parser.map(() => ({ 54 | type: 'Topics', 55 | })), 56 | ); 57 | 58 | const parse = (s: string): Location => 59 | R.parse(router, R.Route.parse(s), { 60 | type: 'NotFound', 61 | }); 62 | 63 | const format = (l: Location): string => { 64 | switch (l.type) { 65 | case 'Home': 66 | return R.format(home.formatter, l); 67 | case 'About': 68 | return R.format(about.formatter, l); 69 | case 'Topics': 70 | return R.format(topics.formatter, l); 71 | case 'TopicsID': 72 | return R.format(topicsID.formatter, l); 73 | case 'NotFound': 74 | return '/'; 75 | } 76 | }; 77 | 78 | const parseExamples = [ 79 | parse('/topics/anything'), 80 | parse('/bad/anything'), 81 | parse('/topics/anything/whoops'), 82 | parse('/topics/What%20will%20happen%3F'), 83 | parse(''), 84 | parse('/'), 85 | parse('about'), 86 | parse('/about'), 87 | parse('about/'), 88 | parse('/about/'), 89 | ]; 90 | parseExamples.forEach(ex => console.log(ex)); 91 | 92 | const formatExamples = [ 93 | format({ 94 | type: 'TopicsID', 95 | id: 'anything', 96 | }), 97 | format({ 98 | type: 'TopicsID', 99 | id: 'What will happen?', 100 | }), 101 | ]; 102 | formatExamples.forEach(ex => console.log(ex)); 103 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fp-ts-routing-examples", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@iadvize-oss/foldable-helpers": "^1.0.4", 7 | "@morphic-ts/adt": "^2.0.0-alpha.4", 8 | "daggy": "^1.4.0", 9 | "express": "^4.17.1", 10 | "fp-ts-routing": "^0.5.3", 11 | "fp-ts-rxjs": "^0.6.9", 12 | "io-ts": "^2.2.7", 13 | "monocle-ts": "^2.1.1", 14 | "morphic-ts-routing": "^0.0.45", 15 | "node-fetch": "^2.6.0", 16 | "react": "^16.13.1", 17 | "react-dom": "^16.13.1", 18 | "react-redux": "^7.2.0", 19 | "react-router-dom": "^5.2.0", 20 | "react-scripts": "3.4.1", 21 | "redux": "^4.0.5", 22 | "redux-observable": "^1.2.0", 23 | "rxjs": "^6.5.5", 24 | "serialize-javascript": "^3.1.0", 25 | "typescript": "~3.9.5", 26 | "unionize": "^3.1.0" 27 | }, 28 | "scripts": { 29 | "start": "react-scripts start", 30 | "build": "react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject", 33 | "dev:build-server": "NODE_ENV=development webpack --config webpack.server.js --mode=development -w", 34 | "dev:start": "nodemon --exec 'node' ./server-build/server.js", 35 | "dev": "npm-run-all --parallel dev:*" 36 | }, 37 | "eslintConfig": { 38 | "extends": "react-app" 39 | }, 40 | "browserslist": { 41 | "production": [ 42 | ">0.2%", 43 | "not dead", 44 | "not op_mini all" 45 | ], 46 | "development": [ 47 | "last 1 chrome version", 48 | "last 1 firefox version", 49 | "last 1 safari version" 50 | ] 51 | }, 52 | "devDependencies": { 53 | "@testing-library/jest-dom": "^4.2.4", 54 | "@testing-library/react": "^9.3.2", 55 | "@testing-library/user-event": "^12.0.2", 56 | "@types/assert": "^1.4.7", 57 | "@types/express": "^4.17.6", 58 | "@types/jest": "^24.0.0", 59 | "@types/node": "^12.0.0", 60 | "@types/node-fetch": "^2.5.7", 61 | "@types/react": "^16.9.0", 62 | "@types/react-dom": "^16.9.0", 63 | "@types/react-redux": "^7.1.9", 64 | "@types/react-router-dom": "^5.1.5", 65 | "@types/serialize-javascript": "^1.5.0", 66 | "assert": "^2.0.0", 67 | "css-loader": "^3.6.0", 68 | "eslint-plugin-testing-library": "^3.3.1", 69 | "file-loader": "^6.0.0", 70 | "nodemon": "^2.0.4", 71 | "npm-run-all": "^4.1.5", 72 | "style-loader": "^1.2.1", 73 | "ts-loader": "^7.0.5", 74 | "webpack-cli": "^3.3.11", 75 | "webpack-node-externals": "^1.7.2" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/examples/3-reactive-architecture-w-redux/3-redux-observable/3-test/2-onpopstate/todorouter.test.tsx: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/lib/pipeable'; 2 | import * as r from 'rxjs'; 3 | import * as ro from 'rxjs/operators'; 4 | import assert from 'assert'; 5 | import epic from '../1-pushState/epic'; 6 | import { AppState, VisibilityFilter } from '../../../1-redux/1-global-state/AppState'; 7 | import { Location, format } from '../../../../2-data-model/4-ssot/Location'; 8 | import { AppAction } from '../../2-pushState/AppAction'; 9 | import { StateObservable, ActionsObservable } from 'redux-observable'; 10 | 11 | test('New filters change the url', async () => { 12 | const href$ = new r.BehaviorSubject('/'); 13 | 14 | const popUrls = [ 15 | Location.of.Completed({ value: {} }), 16 | Location.of.Active({ value: {} }), 17 | Location.of.Landing({ value: {} }), 18 | ] 19 | const popUrls$ = pipe( 20 | popUrls, 21 | a => r.from(a), 22 | ro.map(format), 23 | ); 24 | 25 | const pushActions = [ 26 | AppAction.of.SET_VISIBILITY_FILTER_LOCATION({ 27 | filter: 'SHOW_COMPLETED' 28 | }), 29 | AppAction.of.SET_VISIBILITY_FILTER_LOCATION({ 30 | filter: 'SHOW_ACTIVE' 31 | }), 32 | AppAction.of.SET_VISIBILITY_FILTER_LOCATION({ 33 | filter: 'SHOW_ALL' 34 | }), 35 | ]; 36 | const action$ = pipe( 37 | pushActions, 38 | arr => r.from(arr), 39 | obs => ActionsObservable.from(obs), 40 | ); 41 | const state$ = new StateObservable( 42 | new r.Subject(), 43 | undefined as any, 44 | ); 45 | 46 | const appEpic = epic( 47 | () => {}, 48 | href$, 49 | () => href$.getValue(), 50 | ); 51 | 52 | const emittedActions = await pipe( 53 | appEpic(action$, state$, null), 54 | ro.map((a, index) => { 55 | // after the initial route 56 | if (index === 1) { 57 | popUrls$.subscribe((a) => href$.next(a)) 58 | } 59 | if (index === popUrls.length + 1) { 60 | href$.complete(); 61 | } 62 | return a; 63 | }), 64 | ro.toArray(), 65 | ).toPromise(); 66 | 67 | assert.deepStrictEqual( 68 | emittedActions, 69 | [ 70 | // initial value 71 | 'SHOW_ALL' as VisibilityFilter, 72 | 73 | // popped 74 | 'SHOW_COMPLETED' as VisibilityFilter, 75 | 'SHOW_ACTIVE' as VisibilityFilter, 76 | 'SHOW_ALL' as VisibilityFilter, 77 | 78 | // pushed 79 | 'SHOW_COMPLETED' as VisibilityFilter, 80 | 'SHOW_ACTIVE' as VisibilityFilter, 81 | 'SHOW_ALL' as VisibilityFilter, 82 | ].map( 83 | filter => AppAction.of.SET_VISIBILITY_FILTER({ filter }) 84 | ), 85 | ); 86 | }); -------------------------------------------------------------------------------- /webpack.server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | 4 | const serverConfig = { 5 | entry: './server/server.tsx', 6 | 7 | target: 'node', 8 | 9 | externals: [nodeExternals()], 10 | 11 | output: { 12 | path: path.resolve('server-build'), 13 | filename: 'server.js', 14 | 15 | // Bundle absolute resource paths in the source-map, 16 | // so VSCode can match the source file. 17 | devtoolModuleFilenameTemplate: '[absolute-resource-path]' 18 | }, 19 | 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.tsx?$/, 24 | loader: 'ts-loader', 25 | exclude: /node_modules/, 26 | options: { 27 | allowTsInNodeModules: true, 28 | configFile: 'server.tsconfig.json' 29 | }, 30 | }, 31 | { 32 | test: /\.css$/i, 33 | use: ['css-loader'], 34 | }, 35 | { 36 | test: /\.(jpg|jpeg|png|svg|gif)$/, 37 | use: [{ 38 | loader: 'file-loader', 39 | options: { 40 | name: '[md5:hash:hex].[ext]', 41 | publicPath: '/server-build/img', 42 | outputPath: 'img', 43 | } 44 | }] 45 | } 46 | ] 47 | }, 48 | 49 | devtool: 'source-map', 50 | 51 | resolve: { 52 | extensions: [ '.tsx', '.ts', '.js', '.css', '.svg', '.png' ], 53 | }, 54 | }; 55 | 56 | const clientConfig = { 57 | entry: './server/hydrate.tsx', 58 | 59 | target: 'web', 60 | 61 | output: { 62 | path: path.resolve('server-build'), 63 | filename: 'hydrate.js', 64 | 65 | // Bundle absolute resource paths in the source-map, 66 | // so VSCode can match the source file. 67 | devtoolModuleFilenameTemplate: '[absolute-resource-path]' 68 | }, 69 | 70 | module: { 71 | rules: [ 72 | { 73 | test: /\.(ts|tsx)?$/, 74 | loader: 'ts-loader', 75 | exclude: /node_modules/, 76 | options: { 77 | allowTsInNodeModules: true, 78 | configFile: 'server.tsconfig.json' 79 | }, 80 | }, 81 | { 82 | test: /\.css$/i, 83 | use: ['style-loader', 'css-loader'], 84 | }, 85 | { 86 | test: /\.(jpg|jpeg|png|svg|gif)$/, 87 | use: [{ 88 | loader: 'file-loader', 89 | options: { 90 | name: '[md5:hash:hex].[ext]', 91 | publicPath: '/server-build/img', 92 | outputPath: 'img', 93 | } 94 | }] 95 | } 96 | ] 97 | }, 98 | 99 | devtool: 'source-map', 100 | 101 | resolve: { 102 | extensions: [ '.tsx', '.ts', '.js', '.css', '.svg', '.png' ], 103 | }, 104 | }; 105 | 106 | module.exports = [serverConfig, clientConfig]; -------------------------------------------------------------------------------- /src/examples/2-data-model/3-router/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { TodoType, defaultTodos, VisibilityFilter } from '../1-simple-todo/AppState'; 3 | import AddTodo from '../1-simple-todo/components/AddTodo'; 4 | import TodoList from '../1-simple-todo/components/TodoList'; 5 | import Footer from '../1-simple-todo/components/Footer'; 6 | import { parse, Location, format } from './Location'; 7 | 8 | const routeToVisibilityFilter = Location.matchStrict({ 9 | Landing: () => 'SHOW_ALL', 10 | Active: () => 'SHOW_ACTIVE', 11 | Completed: () => 'SHOW_COMPLETED', 12 | NotFound: () => 'SHOW_ALL', 13 | }) 14 | 15 | export default function App() { 16 | const [todos, setTodos] = useState(defaultTodos); 17 | const [visibilityFilter, setVisibilityFilter] = useState( 18 | routeToVisibilityFilter( 19 | parse( 20 | window.location.pathname 21 | ) 22 | ), 23 | ); 24 | const [location, setLocation] = useState( 25 | parse( 26 | window.location.pathname 27 | ) 28 | ); 29 | window.addEventListener('popstate', () => { 30 | const newLocation = parse(window.location.pathname) 31 | setLocation( 32 | newLocation 33 | ); 34 | setVisibilityFilter( 35 | routeToVisibilityFilter( 36 | newLocation, 37 | ) 38 | ); 39 | }); 40 | const updateVisibilityFilter = (newVisibility: VisibilityFilter) => { 41 | let newRoute: Location; 42 | switch (newVisibility) { 43 | case 'SHOW_ALL': 44 | newRoute = Location.of.Landing({ value: {} }); 45 | break; 46 | case 'SHOW_ACTIVE': 47 | newRoute = Location.of.Active({ value: {} }); 48 | break; 49 | case 'SHOW_COMPLETED': 50 | newRoute = Location.of.Completed({ value: {} }); 51 | break; 52 | } 53 | window.history.pushState(null, '', format(newRoute)) 54 | setVisibilityFilter(newVisibility) 55 | } 56 | return ( 57 |
    58 | { 60 | const highestID = todos.length > 0 61 | ? todos.reduce( 62 | (acc, cur) => cur.id > acc.id 63 | ? cur 64 | : acc 65 | ).id 66 | : 0; 67 | const newTodo: TodoType = { 68 | id: highestID + 1, 69 | text, 70 | completed: false, 71 | } 72 | setTodos([...todos, newTodo]); 73 | }} 74 | /> 75 | { 79 | const newTodos = todos.map( 80 | t => t.id === id 81 | ? { 82 | ...t, 83 | completed: !t.completed, 84 | } 85 | : t 86 | ); 87 | setTodos(newTodos); 88 | }} 89 | /> 90 |
    94 |
    95 | ) 96 | } 97 | 98 | -------------------------------------------------------------------------------- /src/examples/2-data-model/2-react-testing-library/todo.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, cleanup } from '@testing-library/react'; 3 | import userEvent from '@testing-library/user-event'; 4 | import App from '../1-simple-todo/App'; 5 | import { defaultTodos } from '../1-simple-todo/AppState'; 6 | 7 | describe('Todo App', () => { 8 | const newTodoText = 'Hot Dog Eating Contest'; 9 | 10 | beforeEach(() => { 11 | render( 12 | 13 | ); 14 | }); 15 | 16 | // Note: running cleanup afterEach is done automatically for you in @testing-library/react@9.0.0 or higher 17 | // https://jestjs.io/docs/en/tutorial-react 18 | afterEach(cleanup); 19 | 20 | it('Has default todos', () => { 21 | defaultTodos.forEach(t => { 22 | const todoElement = screen.getByRole('listitem', { name: `todo: ${t.text}` }); 23 | expect(todoElement).toBeInTheDocument(); 24 | }); 25 | }); 26 | it('Can add todo', async () => { 27 | const newTodoTextElem = screen.getByRole('textbox', { name: /New Todo Text/i }); 28 | await userEvent.type(newTodoTextElem, newTodoText); 29 | 30 | const addTodoButtonElem = screen.getByRole('button', { name: /Add Todo/i }); 31 | userEvent.click(addTodoButtonElem); 32 | 33 | const newTodoElement = screen.getByRole('listitem', { name: `todo: ${newTodoText}` }); 34 | expect(newTodoElement).toBeInTheDocument(); 35 | }); 36 | it('Can complete/uncomplete todo', () => { 37 | const todoElement = screen.getByRole('listitem', { name: `todo: ${defaultTodos[0].text}` }); 38 | 39 | expect(todoElement).toBeInTheDocument(); 40 | 41 | const styleBeforeClick = window.getComputedStyle(todoElement); 42 | expect(styleBeforeClick.textDecoration).toMatch(/none/i); 43 | 44 | userEvent.click(todoElement); 45 | 46 | const styleAfterClick = window.getComputedStyle(todoElement); 47 | expect(styleAfterClick.textDecoration).toMatch(/line-through/i); 48 | 49 | userEvent.click(todoElement); 50 | 51 | const styleAfterReset = window.getComputedStyle(todoElement); 52 | expect(styleAfterReset.textDecoration).toMatch(/none/i); 53 | }); 54 | it('Can display all/active/completed todos', () => { 55 | const firstTodoElement = screen.getByRole('listitem', { name: `todo: ${defaultTodos[0].text}` }); 56 | expect(firstTodoElement).toBeInTheDocument(); 57 | userEvent.click(firstTodoElement); 58 | 59 | const hasElement = (index: number, has: boolean) => { 60 | const elem = screen.queryByRole('listitem', { name: `todo: ${defaultTodos[index].text}` }); 61 | if (has) { 62 | expect(elem).toBeInTheDocument(); 63 | } else { 64 | expect(elem).not.toBeInTheDocument(); 65 | } 66 | } 67 | const showCompletedTodos = screen.getByRole('button', { name: /Completed/i }); 68 | expect(showCompletedTodos).toBeInTheDocument(); 69 | const showActiveTodos = screen.getByRole('button', { name: /Active/i }); 70 | expect(showActiveTodos).toBeInTheDocument(); 71 | const showAllTodos = screen.getByRole('button', { name: /All/i }); 72 | expect(showAllTodos).toBeInTheDocument(); 73 | 74 | userEvent.click(showActiveTodos); 75 | 76 | hasElement(0, false); 77 | hasElement(1, true); 78 | 79 | userEvent.click(showCompletedTodos); 80 | 81 | hasElement(0, true); 82 | hasElement(1, false); 83 | 84 | userEvent.click(showAllTodos); 85 | 86 | hasElement(0, true); 87 | hasElement(1, true); 88 | }); 89 | }); -------------------------------------------------------------------------------- /src/examples/1-react-router/7-morphic-ts-routing/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import * as R from 'fp-ts-routing'; 3 | import { routingFromMatches4 } from 'morphic-ts-routing'; 4 | import { ADTType } from '@morphic-ts/adt'; 5 | import LocationLink from './LocationLink'; 6 | 7 | export const { 8 | parse, 9 | format, 10 | adt: ParseableLocation 11 | } = routingFromMatches4( 12 | ['Home', R.end], 13 | ['About', R.lit('about').then(R.end)], 14 | ['Topics', R.lit('topics').then(R.end)], 15 | ['TopicsID', R.lit('topics').then(R.str('id')).then(R.end)], 16 | ); 17 | type ParseableLocation = ADTType 18 | 19 | const Location = ParseableLocation.exclude(['NotFound']) 20 | export type Location = ADTType 21 | 22 | const TopicLocation = Location.select([ 23 | 'Topics', 'TopicsID', 24 | ]); 25 | type TopicLocation = ADTType 26 | 27 | export default function App() { 28 | const [location, setLocation] = useState(parse(window.location.pathname)); 29 | const updateLocation = (newLocation: Location) => { 30 | setLocation(newLocation); 31 | window.history.pushState(null, '', format(newLocation)); 32 | } 33 | window.addEventListener('popstate', () => { 34 | setLocation(parse(window.location.pathname)); 35 | }); 36 | return ( 37 |
    38 |
      39 |
    • 40 | 44 | Home 45 | 46 |
    • 47 |
    • 48 | 52 | About 53 | 54 |
    • 55 |
    • 56 | 60 | Topics 61 | 62 |
    • 63 |
    64 | {ParseableLocation.matchStrict({ 65 | Home: () => , 66 | About: () => , 67 | Topics: (l) => , 71 | TopicsID: (l) => , 75 | NotFound: () =>
    , 76 | })(location)} 77 |
    78 | ); 79 | } 80 | 81 | function Home() { 82 | return

    Home

    ; 83 | } 84 | 85 | function About() { 86 | return

    About

    ; 87 | } 88 | 89 | const Topics = ({ 90 | location, 91 | updateLocation, 92 | }: { 93 | location: TopicLocation, 94 | updateLocation: (l: Location) => void, 95 | }) => ( 96 |
    97 |

    Topics

    98 |
      99 |
    • 100 | 104 | Components 105 | 106 |
    • 107 |
    • 108 | 112 | Props v. State 113 | 114 |
    • 115 |
    116 | {TopicLocation.match({ 117 | Topics: () =>

    Please select a topic.

    , 118 | TopicsID: (l) => 119 | })(location)} 120 |
    121 | ); 122 | 123 | function Topic({ 124 | topicId, 125 | }: { 126 | topicId: string 127 | }) { 128 | return

    Requested topic ID: {topicId}

    ; 129 | } 130 | -------------------------------------------------------------------------------- /src/examples/1-react-router/6-morphic-ts/App.tsx: -------------------------------------------------------------------------------- 1 | import { ADTType, makeADT, ofType } from '@morphic-ts/adt'; 2 | import * as R from 'fp-ts-routing'; 3 | import React, { useState } from 'react'; 4 | import LocationLink from './LocationLink'; 5 | 6 | const ParseableLocation = makeADT('type')({ 7 | Home: ofType(), 8 | About: ofType(), 9 | Topics: ofType(), 10 | TopicsID: ofType<{ type: 'TopicsID'; id: string }>(), 11 | NotFound: ofType(), 12 | }); 13 | type ParseableLocation = ADTType; 14 | 15 | export const Location = ParseableLocation.exclude(['NotFound']); 16 | export type Location = ADTType; 17 | 18 | const TopicLocation = Location.select(['Topics', 'TopicsID']); 19 | type TopicLocation = ADTType 20 | 21 | const home = R.end; 22 | const about = R.lit('about').then(R.end); 23 | const topics = R.lit('topics').then(R.end); 24 | const topicsID = R.lit('topics') 25 | .then(R.str('id')) 26 | .then(R.end); 27 | 28 | const router = home.parser 29 | .map(() => Location.of.Home({})) 30 | .alt(about.parser.map(() => Location.of.About({}))) 31 | .alt(topics.parser.map(() => Location.of.Topics({}))) 32 | .alt(topicsID.parser.map(({ id }) => Location.of.TopicsID({ id }))); 33 | 34 | export const parse = (s: string): ParseableLocation => 35 | R.parse(router, R.Route.parse(s), { type: 'NotFound' }); 36 | 37 | export const format = ParseableLocation.matchStrict({ 38 | Home: l => R.format(home.formatter, l), 39 | About: l => R.format(about.formatter, l), 40 | Topics: l => R.format(topics.formatter, l), 41 | TopicsID: l => R.format(topicsID.formatter, l), 42 | NotFound: () => '/', 43 | }); 44 | 45 | export default function App() { 46 | const [location, setLocation] = useState(parse(window.location.pathname)); 47 | const updateLocation = (newLocation: Location) => { 48 | setLocation(newLocation); 49 | window.history.pushState(null, '', format(newLocation)); 50 | } 51 | window.addEventListener('popstate', () => { 52 | setLocation(parse(window.location.pathname)); 53 | }); 54 | return ( 55 |
    56 |
      57 |
    • 58 | 62 | Home 63 | 64 |
    • 65 |
    • 66 | 70 | About 71 | 72 |
    • 73 |
    • 74 | 78 | Topics 79 | 80 |
    • 81 |
    82 | {ParseableLocation.matchStrict({ 83 | Home: () => , 84 | About: () => , 85 | Topics: (l) => , 89 | TopicsID: (l) => , 93 | NotFound: () =>
    , 94 | })(location)} 95 |
    96 | ); 97 | } 98 | 99 | function Home() { 100 | return

    Home

    ; 101 | } 102 | 103 | function About() { 104 | return

    About

    ; 105 | } 106 | 107 | const Topics = ({ 108 | location, 109 | updateLocation, 110 | }: { 111 | location: TopicLocation, 112 | updateLocation: (l: Location) => void, 113 | }) => ( 114 |
    115 |

    Topics

    116 |
      117 |
    • 118 | 122 | Components 123 | 124 |
    • 125 |
    • 126 | 130 | Props v. State 131 | 132 |
    • 133 |
    134 | {TopicLocation.match({ 135 | Topics: () =>

    Please select a topic.

    , 136 | TopicsID: (l) => 137 | })(location)} 138 |
    139 | ); 140 | 141 | function Topic({ 142 | topicId, 143 | }: { 144 | topicId: string 145 | }) { 146 | return

    Requested topic ID: {topicId}

    ; 147 | } 148 | 149 | -------------------------------------------------------------------------------- /src/examples/1-react-router/6-bonus-verified/App.tsx: -------------------------------------------------------------------------------- 1 | import { pipe } from 'fp-ts/lib/pipeable'; 2 | import * as O from 'fp-ts/lib/Option'; 3 | import { ADTType, makeADT, ofType } from '@morphic-ts/adt'; 4 | import * as R from 'fp-ts-routing'; 5 | import React, { useState } from 'react'; 6 | import LocationLink from '../6-morphic-ts/LocationLink'; 7 | 8 | const ParseableLocation = makeADT('type')({ 9 | Home: ofType(), 10 | About: ofType(), 11 | Topics: ofType(), 12 | TopicsID: ofType<{ type: 'TopicsID'; id: string }>(), 13 | NotFound: ofType(), 14 | }); 15 | type ParseableLocation = ADTType; 16 | 17 | export const Location = ParseableLocation.exclude(['NotFound']); 18 | export type Location = ADTType; 19 | 20 | const HomeLocation = Location.select([ 21 | 'Home', 'About', 22 | ]) 23 | type HomeLocation = ADTType 24 | 25 | const TopicLocation = Location.select(['Topics', 'TopicsID']); 26 | type TopicLocation = ADTType 27 | 28 | const home = R.end; 29 | const about = R.lit('about').then(R.end); 30 | const topics = R.lit('topics').then(R.end); 31 | const topicsID = R.lit('topics') 32 | .then(R.str('id')) 33 | .then(R.end); 34 | 35 | const router = home.parser 36 | .map(() => Location.of.Home({})) 37 | .alt(about.parser.map(() => Location.of.About({}))) 38 | .alt(topics.parser.map(() => Location.of.Topics({}))) 39 | .alt(topicsID.parser.map(({ id }) => Location.of.TopicsID({ id }))); 40 | 41 | export const parse = (s: string): ParseableLocation => 42 | R.parse(router, R.Route.parse(s), { type: 'NotFound' }); 43 | 44 | export const format = ParseableLocation.matchStrict({ 45 | Home: l => R.format(home.formatter, l), 46 | About: l => R.format(about.formatter, l), 47 | Topics: l => R.format(topics.formatter, l), 48 | TopicsID: l => R.format(topicsID.formatter, l), 49 | NotFound: () => '/', 50 | }); 51 | 52 | export default function App() { 53 | const [location, setLocation] = useState(parse(window.location.pathname)); 54 | const updateLocation = (newLocation: Location) => { 55 | setLocation(newLocation); 56 | window.history.pushState(null, '', format(newLocation)); 57 | } 58 | window.addEventListener('popstate', () => { 59 | setLocation(parse(window.location.pathname)); 60 | }); 61 | return ( 62 |
    63 |
      64 |
    • 65 | 69 | Home 70 | 71 |
    • 72 |
    • 73 | 77 | About 78 | 79 |
    • 80 |
    • 81 | 85 | Topics 86 | 87 |
    • 88 |
    89 | {pipe( 90 | location as HomeLocation, 91 | O.fromPredicate(HomeLocation.verified), 92 | O.map(HomeLocation.match({ 93 | Home: () => , 94 | About: () => , 95 | })), 96 | O.alt(() => pipe( 97 | location as TopicLocation, 98 | O.fromPredicate(TopicLocation.verified), 99 | O.map(l => ( 100 | 104 | )) 105 | )), 106 | // Not Found 107 | O.getOrElse((): JSX.Element => ), 108 | )} 109 |
    110 | ); 111 | } 112 | 113 | function Home() { 114 | return

    Home

    ; 115 | } 116 | 117 | function About() { 118 | return

    About

    ; 119 | } 120 | 121 | const Topics = ({ 122 | location, 123 | updateLocation, 124 | }: { 125 | location: TopicLocation, 126 | updateLocation: (l: Location) => void, 127 | }) => ( 128 |
    129 |

    Topics

    130 |
      131 |
    • 132 | 136 | Components 137 | 138 |
    • 139 |
    • 140 | 144 | Props v. State 145 | 146 |
    • 147 |
    148 | {TopicLocation.match({ 149 | Topics: () =>

    Please select a topic.

    , 150 | TopicsID: (l) => 151 | })(location)} 152 |
    153 | ); 154 | 155 | function Topic({ 156 | topicId, 157 | }: { 158 | topicId: string 159 | }) { 160 | return

    Requested topic ID: {topicId}

    ; 161 | } 162 | 163 | -------------------------------------------------------------------------------- /src/examples/1-react-router/5-fp-ts-routing/App.tsx: -------------------------------------------------------------------------------- 1 | import * as R from 'fp-ts-routing'; 2 | import React, { useState } from 'react'; 3 | import LocationLink from './LocationLink'; 4 | 5 | interface Home { 6 | readonly type: 'Home' 7 | } 8 | 9 | interface About { 10 | readonly type: 'About' 11 | } 12 | 13 | interface Topics { 14 | readonly type: 'Topics' 15 | } 16 | 17 | interface TopicsID { 18 | readonly type: 'TopicsID' 19 | readonly id: string 20 | } 21 | 22 | interface NotFound { 23 | readonly type: 'NotFound' 24 | } 25 | 26 | export type Location = Home | About | Topics 27 | | TopicsID | NotFound 28 | 29 | const home = R.end 30 | const about = R.lit('about').then(R.end) 31 | const topics = R.lit('topics').then(R.end) 32 | const topicsID = R.lit('topics') 33 | .then(R.str('id')) 34 | .then(R.end) 35 | 36 | const router = /*zero().alt*/home.parser.map( 37 | () => ({ 38 | type: 'Home' 39 | }) 40 | ) 41 | .alt(about.parser.map(() => ({ 42 | type: 'About' 43 | }))) 44 | .alt(topics.parser.map(() => ({ 45 | type: 'Topics' 46 | }))) 47 | .alt(topicsID.parser.map(({ id }) => ({ 48 | type: 'TopicsID', 49 | id, 50 | }))) 51 | 52 | export const parse = (s: string): Location => R.parse( 53 | router, 54 | R.Route.parse(s), 55 | { type: 'NotFound' }, 56 | ) 57 | export const format = (l: Location): string => { 58 | switch (l.type) { 59 | case 'Home': 60 | return R.format(home.formatter, l); 61 | case 'About': 62 | return R.format(about.formatter, l); 63 | case 'Topics': 64 | return R.format(topics.formatter, l); 65 | case 'TopicsID': 66 | return R.format(topicsID.formatter, l); 67 | case 'NotFound': 68 | return '/'; 69 | } 70 | } 71 | 72 | export default function App() { 73 | const [location, setLocation] = useState(parse(window.location.pathname)); 74 | const updateLocation = (newLocation: Location) => { 75 | setLocation(newLocation); 76 | window.history.pushState(null, '', format(newLocation)); 77 | } 78 | window.addEventListener('popstate', () => { 79 | setLocation(parse(window.location.pathname)); 80 | }); 81 | let innerComponent: JSX.Element; 82 | switch (location.type) { 83 | case 'Home': 84 | innerComponent = ; 85 | break; 86 | case 'About': 87 | innerComponent = ; 88 | break; 89 | case 'Topics': 90 | innerComponent = ; 94 | break; 95 | case 'TopicsID': 96 | innerComponent = ; 100 | break; 101 | case 'NotFound': 102 | innerComponent = 103 | } 104 | return ( 105 |
    106 |
      107 |
    • 108 | 114 | Home 115 | 116 |
    • 117 |
    • 118 | 124 | About 125 | 126 |
    • 127 |
    • 128 | 134 | Topics 135 | 136 |
    • 137 |
    138 | {innerComponent} 139 |
    140 | ); 141 | } 142 | 143 | function Home() { 144 | return

    Home

    ; 145 | } 146 | 147 | function About() { 148 | return

    About

    ; 149 | } 150 | 151 | function Topics({ 152 | location, 153 | updateLocation, 154 | }: { 155 | location: Topics | TopicsID, 156 | updateLocation: (l: Location) => void, 157 | }) { 158 | let innerComponent: JSX.Element; 159 | switch (location.type) { 160 | case 'Topics': 161 | innerComponent =

    Please select a topic.

    ; 162 | break; 163 | case 'TopicsID': 164 | innerComponent = ; 165 | break; 166 | } 167 | return ( 168 |
    169 |

    Topics

    170 |
      171 |
    • 172 | 176 | Components 177 | 178 |
    • 179 |
    • 180 | 184 | Props v. State 185 | 186 |
    • 187 |
    188 | {innerComponent} 189 |
    190 | ); 191 | } 192 | 193 | function Topic({ 194 | topicId, 195 | }: { 196 | topicId: string 197 | }) { 198 | return

    Requested topic ID: {topicId}

    ; 199 | } 200 | -------------------------------------------------------------------------------- /src/examples/1-react-router/4-tagged-unions/App.tsx: -------------------------------------------------------------------------------- 1 | import * as R from 'fp-ts-routing'; 2 | import React, { useState } from 'react'; 3 | import LocationLink from './LocationLink'; 4 | 5 | interface Home { 6 | readonly type: 'Home' 7 | } 8 | 9 | interface About { 10 | readonly type: 'About' 11 | } 12 | 13 | interface Topics { 14 | readonly type: 'Topics' 15 | } 16 | 17 | interface TopicsID { 18 | readonly type: 'TopicsID' 19 | readonly id: string 20 | } 21 | 22 | interface NotFound { 23 | readonly type: 'NotFound' 24 | } 25 | 26 | export type Location = Home | About | Topics 27 | | TopicsID | NotFound 28 | 29 | const home = R.end; 30 | const about = R.lit('about').then(R.end); 31 | const topics = R.lit('topics').then(R.end); 32 | const topicsID = R.lit('topics') 33 | .then(R.str('id')) 34 | .then(R.end); 35 | 36 | const router: R.Parser = topicsID.parser 37 | .map(obj => { 38 | return { 39 | type: 'TopicsID', 40 | id: obj.id, 41 | }; 42 | }) 43 | .alt( 44 | home.parser.map(() => { 45 | return { 46 | type: 'Home', 47 | }; 48 | }), 49 | ) 50 | .alt( 51 | about.parser.map(() => ({ 52 | type: 'About', 53 | })), 54 | ) 55 | .alt( 56 | topics.parser.map(() => ({ 57 | type: 'Topics', 58 | })), 59 | ); 60 | 61 | export const parse = (s: string): Location => 62 | R.parse(router, R.Route.parse(s), { type: 'NotFound' }); 63 | 64 | export const format = (l: Location): string => { 65 | switch (l.type) { 66 | case 'Home': 67 | return R.format(home.formatter, l); 68 | case 'About': 69 | return R.format(about.formatter, l); 70 | case 'Topics': 71 | return R.format(topics.formatter, l); 72 | case 'TopicsID': 73 | return R.format(topicsID.formatter, l); 74 | case 'NotFound': 75 | return '/'; 76 | } 77 | }; 78 | 79 | export default function App() { 80 | const [location, setLocation] = useState(parse(window.location.pathname)); 81 | const updateLocation = (newLocation: Location) => { 82 | setLocation(newLocation); 83 | window.history.pushState(null, '', format(newLocation)); 84 | } 85 | window.addEventListener('popstate', () => { 86 | setLocation(parse(window.location.pathname)); 87 | }); 88 | let innerComponent: JSX.Element; 89 | switch (location.type) { 90 | case 'Home': 91 | innerComponent = ; 92 | break; 93 | case 'About': 94 | innerComponent = ; 95 | break; 96 | case 'Topics': 97 | innerComponent = ; 101 | break; 102 | case 'TopicsID': 103 | innerComponent = ; 107 | break; 108 | case 'NotFound': 109 | innerComponent = 110 | } 111 | return ( 112 |
    113 |
      114 |
    • 115 | 121 | Home 122 | 123 |
    • 124 |
    • 125 | 131 | About 132 | 133 |
    • 134 |
    • 135 | 141 | Topics 142 | 143 |
    • 144 |
    145 | {innerComponent} 146 |
    147 | ); 148 | } 149 | 150 | function Home() { 151 | return

    Home

    ; 152 | } 153 | 154 | function About() { 155 | return

    About

    ; 156 | } 157 | 158 | function Topics({ 159 | location, 160 | updateLocation, 161 | }: { 162 | location: Topics | TopicsID, 163 | updateLocation: (l: Location) => void, 164 | }) { 165 | let innerComponent: JSX.Element; 166 | switch (location.type) { 167 | case 'Topics': 168 | innerComponent =

    Please select a topic.

    ; 169 | break; 170 | case 'TopicsID': 171 | innerComponent = ; 172 | break; 173 | } 174 | return ( 175 |
    176 |

    Topics

    177 |
      178 |
    • 179 | 183 | Components 184 | 185 |
    • 186 |
    • 187 | 191 | Props v. State 192 | 193 |
    • 194 |
    195 | {innerComponent} 196 |
    197 | ); 198 | } 199 | 200 | function Topic({ 201 | topicId, 202 | }: { 203 | topicId: string 204 | }) { 205 | return

    Requested topic ID: {topicId}

    ; 206 | } 207 | 208 | 209 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 25 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 26 | }; 27 | 28 | export function register(config?: Config) { 29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 30 | // The URL constructor is available in all browsers that support SW. 31 | const publicUrl = new URL( 32 | process.env.PUBLIC_URL, 33 | window.location.pathname 34 | ); 35 | if (publicUrl.origin !== window.location.origin) { 36 | // Our service worker won't work if PUBLIC_URL is on a different origin 37 | // from what our page is served on. This might happen if a CDN is used to 38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 39 | return; 40 | } 41 | 42 | window.addEventListener('load', () => { 43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 44 | 45 | if (isLocalhost) { 46 | // This is running on localhost. Let's check if a service worker still exists or not. 47 | checkValidServiceWorker(swUrl, config); 48 | 49 | // Add some additional logging to localhost, pointing developers to the 50 | // service worker/PWA documentation. 51 | navigator.serviceWorker.ready.then(() => { 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then(registration => { 69 | registration.onupdatefound = () => { 70 | const installingWorker = registration.installing; 71 | if (installingWorker == null) { 72 | return; 73 | } 74 | installingWorker.onstatechange = () => { 75 | if (installingWorker.state === 'installed') { 76 | if (navigator.serviceWorker.controller) { 77 | // At this point, the updated precached content has been fetched, 78 | // but the previous service worker will still serve the older 79 | // content until all client tabs are closed. 80 | console.log( 81 | 'New content is available and will be used when all ' + 82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 83 | ); 84 | 85 | // Execute callback 86 | if (config && config.onUpdate) { 87 | config.onUpdate(registration); 88 | } 89 | } else { 90 | // At this point, everything has been precached. 91 | // It's the perfect time to display a 92 | // "Content is cached for offline use." message. 93 | console.log('Content is cached for offline use.'); 94 | 95 | // Execute callback 96 | if (config && config.onSuccess) { 97 | config.onSuccess(registration); 98 | } 99 | } 100 | } 101 | }; 102 | }; 103 | }) 104 | .catch(error => { 105 | console.error('Error during service worker registration:', error); 106 | }); 107 | } 108 | 109 | function checkValidServiceWorker(swUrl: string, config?: Config) { 110 | // Check if the service worker can be found. If it can't reload the page. 111 | fetch(swUrl, { 112 | headers: { 'Service-Worker': 'script' } 113 | }) 114 | .then(response => { 115 | // Ensure service worker exists, and that we really are getting a JS file. 116 | const contentType = response.headers.get('content-type'); 117 | if ( 118 | response.status === 404 || 119 | (contentType != null && contentType.indexOf('javascript') === -1) 120 | ) { 121 | // No service worker found. Probably a different app. Reload the page. 122 | navigator.serviceWorker.ready.then(registration => { 123 | registration.unregister().then(() => { 124 | window.location.reload(); 125 | }); 126 | }); 127 | } else { 128 | // Service worker found. Proceed as normal. 129 | registerValidSW(swUrl, config); 130 | } 131 | }) 132 | .catch(() => { 133 | console.log( 134 | 'No internet connection found. App is running in offline mode.' 135 | ); 136 | }); 137 | } 138 | 139 | export function unregister() { 140 | if ('serviceWorker' in navigator) { 141 | navigator.serviceWorker.ready 142 | .then(registration => { 143 | registration.unregister(); 144 | }) 145 | .catch(error => { 146 | console.error(error.message); 147 | }); 148 | } 149 | } 150 | --------------------------------------------------------------------------------