├── .github └── workflows │ └── main.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples └── typescript │ ├── .gitignore │ ├── README.md │ ├── components │ ├── Layout.tsx │ └── ProgressBar.tsx │ ├── cypress.json │ ├── cypress │ ├── fixtures │ │ └── example.json │ ├── integration │ │ └── e2e.spec.ts │ ├── plugins │ │ └── .gitkeep │ ├── support │ │ ├── index.d.ts │ │ └── index.ts │ └── tsconfig.json │ ├── global.d.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── formik-scaffold.tsx │ ├── index.tsx │ ├── nested.tsx │ └── vanilla.tsx │ ├── public │ ├── favicon.ico │ └── vercel.svg │ ├── styles │ └── globals.css │ ├── tsconfig.json │ ├── utils │ ├── db.ts │ └── prettyDate.tsx │ └── yarn.lock ├── next-env.d.ts ├── package.json ├── pages └── index.tsx ├── src ├── assertOnServer.ts ├── createForm.tsx ├── getPostBody.tsx ├── index.test.tsx ├── index.tsx ├── objectUtils.test.ts └── objectUtils.ts ├── tsconfig.json ├── tsconfig.tsdx.json └── yarn.lock /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, & test lib 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | node: ['14.x'] # ['12.x', '14.x'] 10 | os: [ubuntu-latest, windows-latest] # [ubuntu-latest, windows-latest, macOS-latest] 11 | 12 | steps: 13 | - name: Checkout repo 14 | uses: actions/checkout@v2 15 | 16 | - name: Use Node ${{ matrix.node }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node }} 20 | 21 | - name: Get yarn cache directory path 22 | id: yarn-cache-dir-path 23 | run: echo "::set-output name=dir::$(yarn cache dir)" 24 | - name: Cache node_modules 25 | id: yarn-cache 26 | uses: actions/cache@v2 27 | with: 28 | path: | 29 | ${{ steps.yarn-cache-dir-path.outputs.dir }} 30 | **/node_modules 31 | /home/runner/.cache/Cypress 32 | C:\Users\runneradmin\AppData\Local\Cypress\Cache 33 | **/.next/cache 34 | key: ${{ runner.os }}-${{ runner.node_version}}-yarn-v4-${{ hashFiles('yarn.lock') }} 35 | restore-keys: | 36 | ${{ runner.os }}-${{ runner.node_version}}-yarn-v4- 37 | 38 | - name: Install dependencies 39 | run: yarn install --frozen-lockfile --silent --ignore-scripts 40 | env: 41 | CI: true 42 | 43 | - name: Lint 44 | run: yarn lint 45 | 46 | - name: Test 47 | run: yarn test --ci --coverage --maxWorkers=2 48 | 49 | - name: Build 50 | run: yarn build 51 | 52 | typescript-example: 53 | name: Build & Run Cypress on Example 54 | defaults: 55 | run: 56 | working-directory: examples/typescript 57 | runs-on: ${{ matrix.os }} 58 | strategy: 59 | matrix: 60 | node: ['14.x'] # ['12.x', '14.x'] 61 | os: [ubuntu-latest] # [ubuntu-latest, windows-latest, macOS-latest] 62 | 63 | steps: 64 | - name: Checkout repo 65 | uses: actions/checkout@v2 66 | 67 | - name: Use Node ${{ matrix.node }} 68 | uses: actions/setup-node@v1 69 | with: 70 | node-version: ${{ matrix.node }} 71 | 72 | - name: Get yarn cache directory path 73 | id: yarn-cache-dir-path 74 | run: echo "::set-output name=dir::$(yarn cache dir)" 75 | - name: Cache node_modules 76 | id: yarn-cache 77 | uses: actions/cache@v2 78 | with: 79 | path: | 80 | ${{ steps.yarn-cache-dir-path.outputs.dir }} 81 | **/node_modules 82 | /home/runner/.cache/Cypress 83 | C:\Users\runneradmin\AppData\Local\Cypress\Cache 84 | **/.next/cache 85 | key: ${{ runner.os }}-${{ runner.node_version}}-yarn-typescript-example-v4-${{ hashFiles('yarn.lock') }} 86 | restore-keys: | 87 | ${{ runner.os }}-${{ runner.node_version}}-yarn-typescript-example-v4- 88 | 89 | # - name: Install/link lib 90 | # run: | 91 | # cd ../../ 92 | # yarn --frozen-lockfile --silent 93 | # yarn link 94 | # env: 95 | # CI: true 96 | 97 | - name: Install 98 | run: | 99 | yarn --silent 100 | env: 101 | CI: true 102 | 103 | - name: Build 104 | run: yarn build 105 | 106 | - name: Cypress run 107 | uses: cypress-io/github-action@v2 108 | with: 109 | start: yarn start 110 | working-directory: examples/typescript 111 | -------------------------------------------------------------------------------- /.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 | # next.js 12 | .next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | dist/ 22 | package-lock.json 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env.local 31 | .env.development.local 32 | .env.test.local 33 | .env.production.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | # cypress 39 | **/cypress/videos 40 | **/cypress/screenshots 41 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "yzhang.markdown-all-in-one"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # TSDX React User Guide 2 | 3 | Congrats! You just saved yourself hours of work by bootstrapping this project with TSDX. Let’s get you oriented with what’s here and how to use it. 4 | 5 | > This TSDX setup is meant for developing React component libraries (not apps!) that can be published to NPM. If you’re looking to build a React-based app, you should use `create-react-app`, `razzle`, `nextjs`, `gatsby`, or `react-static`. 6 | 7 | > If you’re new to TypeScript and React, checkout [this handy cheatsheet](https://github.com/sw-yx/react-typescript-cheatsheet/) 8 | 9 | ## Commands 10 | 11 | TSDX scaffolds your new library inside `/src`, and also sets up a [Parcel-based](https://parceljs.org) playground for it inside `/example`. 12 | 13 | The recommended workflow is to run TSDX in one terminal: 14 | 15 | ```bash 16 | npm start # or yarn start 17 | ``` 18 | 19 | This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`. 20 | 21 | Then run the example inside another: 22 | 23 | ```bash 24 | cd example 25 | npm i # or yarn to install dependencies 26 | npm start # or yarn start 27 | ``` 28 | 29 | The default example imports and live reloads whatever is in `/dist`, so if you are seeing an out of date component, make sure TSDX is running in watch mode like we recommend above. **No symlinking required**, we use [Parcel's aliasing](https://parceljs.org/module_resolution.html#aliases). 30 | 31 | To do a one-off build, use `npm run build` or `yarn build`. 32 | 33 | To run tests, use `npm test` or `yarn test`. 34 | 35 | ## Configuration 36 | 37 | Code quality is set up for you with `prettier`, `husky`, and `lint-staged`. Adjust the respective fields in `package.json` accordingly. 38 | 39 | ### Jest 40 | 41 | Jest tests are set up to run with `npm test` or `yarn test`. 42 | 43 | ### Bundle analysis 44 | 45 | Calculates the real cost of your library using [size-limit](https://github.com/ai/size-limit) with `npm run size` and visulize it with `npm run analyze`. 46 | 47 | #### Setup Files 48 | 49 | This is the folder structure we set up for you: 50 | 51 | ```txt 52 | /example 53 | index.html 54 | index.tsx # test your component here in a demo app 55 | package.json 56 | tsconfig.json 57 | /src 58 | index.tsx # EDIT THIS 59 | /test 60 | blah.test.tsx # EDIT THIS 61 | .gitignore 62 | package.json 63 | README.md # EDIT THIS 64 | tsconfig.json 65 | ``` 66 | 67 | #### React Testing Library 68 | 69 | We do not set up `react-testing-library` for you yet, we welcome contributions and documentation on this. 70 | 71 | ### Rollup 72 | 73 | TSDX uses [Rollup](https://rollupjs.org) as a bundler and generates multiple rollup configs for various module formats and build settings. See [Optimizations](#optimizations) for details. 74 | 75 | ### TypeScript 76 | 77 | `tsconfig.json` is set up to interpret `dom` and `esnext` types, as well as `react` for `jsx`. Adjust according to your needs. 78 | 79 | ## Continuous Integration 80 | 81 | ### GitHub Actions 82 | 83 | Two actions are added by default: 84 | 85 | - `main` which installs deps w/ cache, lints, tests, and builds on all pushes against a Node and OS matrix 86 | - `size` which comments cost comparison of your library on every pull request using [`size-limit`](https://github.com/ai/size-limit) 87 | 88 | ## Optimizations 89 | 90 | Please see the main `tsdx` [optimizations docs](https://github.com/palmerhq/tsdx#optimizations). In particular, know that you can take advantage of development-only optimizations: 91 | 92 | ```js 93 | // ./types/index.d.ts 94 | declare var __DEV__: boolean; 95 | 96 | // inside your code... 97 | if (__DEV__) { 98 | console.log('foo'); 99 | } 100 | ``` 101 | 102 | You can also choose to install and use [invariant](https://github.com/palmerhq/tsdx#invariant) and [warning](https://github.com/palmerhq/tsdx#warning) functions. 103 | 104 | ## Module Formats 105 | 106 | CJS, ESModules, and UMD module formats are supported. 107 | 108 | The appropriate paths are configured in `package.json` and `dist/index.js` accordingly. Please report if any issues are found. 109 | 110 | ## Deploying the Example Playground 111 | 112 | The Playground is just a simple [Parcel](https://parceljs.org) app, you can deploy it anywhere you would normally deploy that. Here are some guidelines for **manually** deploying with the Netlify CLI (`npm i -g netlify-cli`): 113 | 114 | ```bash 115 | cd example # if not already in the example folder 116 | npm run build # builds to dist 117 | netlify deploy # deploy the dist folder 118 | ``` 119 | 120 | Alternatively, if you already have a git repo connected, you can set up continuous deployment with Netlify: 121 | 122 | ```bash 123 | netlify init 124 | # build command: yarn build && cd example && yarn && yarn build 125 | # directory to deploy: example/dist 126 | # pick yes for netlify.toml 127 | ``` 128 | 129 | ## Named Exports 130 | 131 | Per Palmer Group guidelines, [always use named exports.](https://github.com/palmerhq/typescript#exports) Code split inside your React app instead of your React library. 132 | 133 | ## Including Styles 134 | 135 | There are many ways to ship styles, including with CSS-in-JS. TSDX has no opinion on this, configure how you like. 136 | 137 | For vanilla CSS, you can include it at the root directory and add it to the `files` section in your `package.json`, so that it can be imported separately by your users and run through their bundler's loader. 138 | 139 | ## Publishing to NPM 140 | 141 | We recommend using [np](https://github.com/sindresorhus/np). 142 | 143 | ## Usage with Lerna 144 | 145 | When creating a new package with TSDX within a project set up with Lerna, you might encounter a `Cannot resolve dependency` error when trying to run the `example` project. To fix that you will need to make changes to the `package.json` file _inside the `example` directory_. 146 | 147 | The problem is that due to the nature of how dependencies are installed in Lerna projects, the aliases in the example project's `package.json` might not point to the right place, as those dependencies might have been installed in the root of your Lerna project. 148 | 149 | Change the `alias` to point to where those packages are actually installed. This depends on the directory structure of your Lerna project, so the actual path might be different from the diff below. 150 | 151 | ```diff 152 | "alias": { 153 | - "react": "../node_modules/react", 154 | - "react-dom": "../node_modules/react-dom" 155 | + "react": "../../../node_modules/react", 156 | + "react-dom": "../../../node_modules/react-dom" 157 | }, 158 | ``` 159 | 160 | An alternative to fixing this problem would be to remove aliases altogether and define the dependencies referenced as aliases as dev dependencies instead. [However, that might cause other problems.](https://github.com/palmerhq/tsdx/issues/64) 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 KATT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `next-ssr-form` 2 | 3 | > **⚠️⚠️ This is an experimental library and is likely to be discontinued ⚠️⚠️** 4 | 5 | ## About 6 | 7 | - 🔨 Uses Next.js' `getServerSideProps` to receive post data and provide helpers to the page to render the form 8 | - 🎷 E2E type safety! TypeScript types inferred between client & server with all the nice autocomplete jazz 9 | - 🔐 Server-side data validation that's propagated to page props & your `mutation`s' return values are inferred. 10 | - ☁️ Allows you to write data straight to the db with confidence 11 | - 🤘 Your page will wors without JS enabled (if you want it to) 12 | 13 | **(Peer) Dependencies:** 14 | 15 | - [zod](https://github.com/colinhacks/zod) for data validation 16 | - [Formik](https://github.com/formium/formik) as the form library 17 | 18 | 19 | **Table of contents:** 20 | 21 | - [`next-ssr-form`](#next-ssr-form) 22 | - [About](#about) 23 | - [Get started](#get-started) 24 | - [0. Install](#0-install) 25 | - [1. Add form to top of page](#1-add-form-to-top-of-page) 26 | - [2. Add mutation to `getServerSideProps`](#2-add-mutation-to-getserversideprops) 27 | - [3. Infer data types](#3-infer-data-types) 28 | - [4. Use form](#4-use-form) 29 | - [Author](#author) 30 | ## Get started 31 | 32 | > ℹ️ Easiest thing to do is to look at the pages in [`examples/typescript`](./examples/typescript). 33 | 34 | ### 0. Install 35 | ```bash 36 | yarn add next-ssr-form zod formik 37 | ``` 38 | 39 | ### 1. Add form to top of page 40 | 41 | In a Next.js `page/..`: 42 | 43 | ```tsx 44 | export const createPostForm = createForm({ 45 | schema: z.object({ 46 | from: z.string().min(2), 47 | message: z.string().min(4), 48 | }), 49 | defaultValues: { 50 | message: "", 51 | from: "", 52 | }, 53 | formId: "createPost", 54 | }); 55 | ``` 56 | 57 | ### 2. Add mutation to `getServerSideProps` 58 | 59 | 60 | ```tsx 61 | export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { 62 | const createPostProps = await createPostForm.getPageProps({ 63 | ctx, 64 | /** 65 | * Your mutation function 66 | * - Will only be called when there's a POST to this form based on the `formId` 67 | * - 🌟 The `input` will be validated by the schema & the types inferred! 68 | */ 69 | async mutation(input) { 70 | return DB.createPost(input); 71 | }, 72 | }); 73 | 74 | return { 75 | props: { 76 | // spread properties onto the prop 77 | ...createPostProps, 78 | posts: await DB.getAllPosts(), 79 | }, 80 | }; 81 | }; 82 | ``` 83 | 84 | ### 3. Infer data types 85 | 86 | ```tsx 87 | type Props = InferGetServerSidePropsType; 88 | 89 | export default function Home(props: Props) { 90 | // ... 91 | ``` 92 | 93 | Your data usage is now typesafe! 94 | 95 | ### 4. Use form 96 | 97 | - Formik: Simple example with code scaffolding: [`pages/formik-scaffold.tsx`](./pages/formik-scaffold.tsx) or 98 | - Formik: More verbose example: [`pages/index.tsx`](./pages/index.tsx) 99 | - Without form library: [`pages/vanilla.tsx`](./pages/vanilla.tsx) 100 | 101 |
102 | Basic form w/o JS 103 | 104 | ```tsx 105 | 106 | type Props = InferGetServerSidePropsType; 107 | export default function Home(props: Props) { 108 | const [feedback, setFeedback] = useState( 109 | createPostForm.getFeedbackFromProps(props) 110 | ); 111 | const initalValues = createPostForm.getInitialValues(props); 112 | const initialErrors = createPostForm.getInitialErrors(props); 113 | 114 | return ( 115 | <> 116 |

Normal http post (zod for validation)

117 |

118 | Uses a standard <form> with the action 119 | -attribute to post to the same page. Form data is handled in{' '} 120 | getServerSideProps and feedback is passed through page 121 | props. 122 |

123 |

My guestbook

124 | {props.posts.map(item => ( 125 |
126 | 127 | From {item.from} at {item.createdAt}: 128 | 129 |

{item.message}

130 |
131 | ))} 132 |

Add post

133 | 134 |
135 |

136 | 144 |

145 |

146 |