From 7ff8942f3fbaee3d52b544ee2b1b52f25a4b41c9 Mon Sep 17 00:00:00 2001 From: codinsonn Date: Sun, 10 Dec 2023 01:13:01 +0100 Subject: [PATCH] feat: Update Aetherspace Docs, add Form Management Docs --- .storybook/docs/Forms.stories.mdx | 11 ++ .storybook/main.js | 1 + packages/@aetherspace/core/README.md | 83 +++++---- .../docs/helpers/StorybookLinkTransformer.tsx | 3 +- packages/@aetherspace/forms/README.md | 166 ++++++++++++++++++ .../navigation/AetherPage/README.md | 37 +++- packages/@aetherspace/navigation/README.md | 16 +- packages/@aetherspace/schemas/README.md | 1 + 8 files changed, 268 insertions(+), 50 deletions(-) create mode 100644 .storybook/docs/Forms.stories.mdx create mode 100644 packages/@aetherspace/forms/README.md diff --git a/.storybook/docs/Forms.stories.mdx b/.storybook/docs/Forms.stories.mdx new file mode 100644 index 00000000..7c0bf12a --- /dev/null +++ b/.storybook/docs/Forms.stories.mdx @@ -0,0 +1,11 @@ +import { Meta } from '@storybook/addon-docs' +import StorybookLinkTransformer from '../../packages/@aetherspace/docs/helpers/StorybookLinkTransformer' +import StorybookFontTransformer from '../../packages/@aetherspace/docs/helpers/StorybookFontTransformer' +import FormManagementMD from '../../packages/@aetherspace/forms/README.md' + + + + + + + diff --git a/.storybook/main.js b/.storybook/main.js index 740bc3f7..f5d92036 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -11,6 +11,7 @@ module.exports = { './docs/Styling.stories.mdx', './docs/UniversalRouting.stories.mdx', './docs/GraphQL.stories.mdx', + './docs/Forms.stories.mdx', './docs/Automation.stories.mdx', './docs/Icons.stories.mdx', './docs/Deployment.stories.mdx', diff --git a/packages/@aetherspace/core/README.md b/packages/@aetherspace/core/README.md index 28f325db..8e1e6e80 100644 --- a/packages/@aetherspace/core/README.md +++ b/packages/@aetherspace/core/README.md @@ -1,4 +1,9 @@ -# Universal from the start +# Aetherspace Core Concepts + +To help you decide if Aetherspace is the right tool for you or your project, +I've written this page to explain the core opinions and benefits when using Aetherspace: + +## Full-Product Universal Apps from the start

@@ -17,26 +22,25 @@ The dream of React development has always been “write-once, use anywhere”. -With Aetherspace, you can apply that concept to building with Expo, React-Native and Next.js, and have a Web, iOS and Android app from the get-go. Maximising your reach from the start. +With Aetherspace, you can apply that concept to building with Expo, React-Native and Next.js, and have a Web, iOS and Android app from the get-go. You'll be maximising your reach right from project kickstart by forking the Aetherspace repo. -> Users increasingly prefer mobile apps over the web. That is just fact. SEO however, is still the best option for organic traffic. But things like push notifications and taking up a spot on the users smartphone, allow for higher conversions overall. By choosing Aetherspace, you immediately get the best of both worlds and set your project up for cross-platform success. +> While SEO is still the best option for new organic traffic for your project, daily active users will increasingly prefer a mobile app for use on the fly. Things like push notifications and taking up a spot on the users smartphone, will also allow for higher conversions overall. Choosing Aetherspace, you immediately get the best of both worlds and are set up for cross-platform success. -Therefore, using the helpers and scripts from `packages/@aetherspace` enables this write-once, reuse anywhere pattern for: -- Routing -- UI (Components, Styling) -- Business logic (Front & Backend) +Therefore, using the helpful resources from `packages/@aetherspace` enables this write-once, reuse anywhere pattern for: +- Routing and Universal Links - for maximum shareability & bookmarkability +- UI - with fully cross-platform Components and styling +- Business logic - Animations -- Icons, assets, ... -- and more... +- Icons, assets and more ...while still being optimised for each platform you're targeting. > "Web vs. Native is dead. Web **and** Native is the future." > - Evan Bacon, expo-router maintainer -# Take what works, make it better +## Take what works, make it better -One way we achieve building universal apps from the start is by taking what works and making it better. We feel like it would be huge waste to throw away a decade of open-source (and e.g. comporated to Flutter, native) learnings by rewriting everything from the ground up. Instead, we optimize by combining existing tools, patching in cross-platform support or expanding them with supersets. +One way I achieve building universal apps from the start is by taking what works and making it better. We feel like it would be huge waste to throw away a decade of open-source learnings by rewriting everything from the ground up. Instead, I optimize by combining existing tools, patching in cross-platform support or expanding them with supersets. ### The GREEN stack @@ -70,11 +74,11 @@ Aside from those core technologies, Aetherspace also sets you up with and is bui - `@expo/html-elements`, for semantic HTML while still using React-Native - `Storybook`, for interactive documentation that drives adoption -These are opinionated choices, but best-in-class ones that we're convinced are here to stay. +These are opinionated choices, but best-in-class ones that I'm convinced are here to stay. Note that all other tool decisions are completely up to you and can be installed in any workspace manually or, if available in the [premium version](/LICENSE.md), merged through handy plugin branches. That means you can bring your own preferred state management, testing, database, and other choices and still benefit from the universal setup. -# Single sources of truth +## Thinking in Single sources of truth

@@ -85,19 +89,38 @@ Note that all other tool decisions are completely up to you and can be installed To further help keep things write-once and not repeat yourself, we’ve chosen Zod, a typescript-first schema validation library, as the way to define your data-structure just once for all your: - Types and in-editor hints - Resolver Arguments and Responses +- Form states and validations - GraphQL types -- Documentation controls - Component props +- Documentation controls Anything you can define in Typescript, you can define with Zod. Check out some examples on our [Schemas and Single Sources of Truth](/packages/@aetherspace/schemas/README.md) docs page. -# Designed for copy-paste +## Documentation drives adoption. + +

+ + Docs with Storybook + +

+ +A great quote by Storybook and the reason Aetherspace comes with it already set-up for you. Because down the road, when you’re scaling and bringing in new developers, the easier it is for new people to know what’s already available, the faster they can be onboarded. (and the less likely they are to reinvent the wheel) + +Docs take time however, and it’s easy to get caught up putting a lot of effort writing docs. When you’re a startup or scaling, it’s not necessarily the thing you’d want to put so much time into. You need to be building first and foremost. So, in essence, when you haven't shipped anything yet: + +> The best docs are the ones you don’t have to write yourself. + +And this is where Aetherspace, using single sources of truth and Storybook are a great match. Using Aetherspace, documentation becomes just a side-effect of you writing zod schemas to describe and type your component’s props. Our scripts will pick-up on that and generate storybook files with interactive controls and descriptions for you. + +You can read more about all of this in the [Single sources of truth](/packages/@aetherspace/schemas/README.md) and [Automations](/packages/@aetherspace/scripts/README.md) docs. + +## Customisable, but designed for copy-paste: We want your fork of the Aetherspace template repo to evolve into your own personalised template repo you can use for most of your projects. However, not every project is the same, which is why the monorepo setup promotes colocating UI, business logic, routing and assets by `/features/` and `/packages/` workspaces. Ideally, you want to be able to merge or copy-paste these folders into a new project and have it just work out of the box. -To facilitate this, we suggest you keep the following folder structure Aetherspace comes with: +To facilitate this, I suggest you keep the following folder structure Aetherspace comes with: ```shell │── features/ @@ -141,7 +164,7 @@ This way, thanks to the startup scripts, copying a folder into another project w - Bring in all related assets and automatically copy them to the public dir(s) - Add autogenerated docs for that feature or package’s components -…and ofcourse allow you to import any other reusables from that package or feature +…and ofcourse allow you to import any other reusables from that package or feature. ### Why not NPM packages? @@ -151,24 +174,6 @@ A major benefit of going with copy-pastable or mergeable folders for recurring f Similar to adding recurring features, removing features or packages from a freshly forked repo then also becomes as simple as removing a folder. -# "Documentation drives adoption" - -

- - Docs with Storybook - -

- -A great quote by Storybook and the reason Aetherspace comes with it already set-up for you. Because down the road, when you’re scaling and bringing in new developers, the easier it is for new people to know what’s already available, the faster they can be onboarded. (and the less likely they are to reinvent the wheel) - -Docs take time however, and it’s easy to get caught up putting a lot of effort writing docs. When you’re a startup or scaling, it’s not necessarily the thing you’d want to put so much time into. You need to be building first and foremost. So, in essence, when you haven't shipped anything yet: - -> The best docs are the ones you don’t have to write yourself. - -And this is where Aetherspace, using single sources of truth and Storybook are a great match. Using Aetherspace, documentation becomes just a side-effect of you writing zod schemas to describe and type your component’s props. Our scripts will pick-up on that and generate storybook files with interactive controls and descriptions for you. - -You can read more about all of this in the [Single sources of truth](/packages/@aetherspace/schemas/README.md) and [Automations](/packages/@aetherspace/scripts/README.md) docs. - ## Getting started with Aetherspace - [Quickstart](/packages/@aetherspace/README.md) @@ -184,18 +189,18 @@ You can read more about all of this in the [Single sources of truth](/packages/@ --- -We firmly believe the opinionated toolbelt and core-concepts provided by the template repo will bring major benefits in terms of speed and efficiency. +I firmly believe the opinionated toolbelt and core-concepts provided by the template repo will bring major benefits in terms of speed and efficiency. However, if you wish, you can actually ignore most of these core-concepts Aetherspace promotes and still benefit (only) from the universal setup. For example: - You can avoid using `SWR`, `@expo/html-elements` or even `Zod` schemas yourself, even when keeping automations -- Ignoring file and folder conventions is fine, automations will just do nothing and you'll need to link routes on your own +- Ignoring file and folder conventions is fine, automations will just ignore those files, but you'll need to link routes on your own - You can ignore the primitives and `tailwind` styling and bring your own preferred styling solution instead -- Ignoring the `graphResolver` or other named exports is fine, but you'll need to bring your own GraphQL setup then +- Ignoring the `graphResolver` or other named exports is fine, but you'll need to bring your own GraphQL setup - If you don't care for docs at all, you can remove the `.storybook/` folder and disable all automations in `next.config.js` -Though, if you do, you might be better served with a Tamgui or Solito starter instead. +Though, if you do, you might be better served with a Tamagui or Solito starter instead. diff --git a/packages/@aetherspace/docs/helpers/StorybookLinkTransformer.tsx b/packages/@aetherspace/docs/helpers/StorybookLinkTransformer.tsx index 02fcd136..5d17e018 100644 --- a/packages/@aetherspace/docs/helpers/StorybookLinkTransformer.tsx +++ b/packages/@aetherspace/docs/helpers/StorybookLinkTransformer.tsx @@ -22,7 +22,7 @@ const StorybookLinkTransformer = (props) => { // Props const { children } = props - // -- Memoizations -- + // -- Effects -- useEffect(() => { transformLinks({ @@ -36,6 +36,7 @@ const StorybookLinkTransformer = (props) => { '?path=/packages/@aetherspace/styles/README.md': '?path=/story/aetherspace-cross-platform-styling--page', // prettier-ignore '?path=/packages/@aetherspace/navigation/README.md': '?path=/docs/aetherspace-universal-routing--page', // prettier-ignore '?path=/packages/@aetherspace/navigation/AetherPage/README.md': '?path=/docs/aetherspace-graphql-data-fetching--page', // prettier-ignore + '?path=/packages/@aetherspace/forms/README.md': '?path=/docs/aetherspace-form-management--page', // prettier-ignore '?path=/packages/@aetherspace/components/AetherIcon/README.md': '?path=/docs/aetherspace-icon-management--page', // prettier-ignore '?path=/packages/@aetherspace/scripts/README.md': '?path=/docs/aetherspace-automation--page', '?path=/packages/@aetherspace/schemas/README.md': '?path=/docs/aetherspace-single-sources-of-truth--page', // prettier-ignore diff --git a/packages/@aetherspace/forms/README.md b/packages/@aetherspace/forms/README.md new file mode 100644 index 00000000..94f22d9a --- /dev/null +++ b/packages/@aetherspace/forms/README.md @@ -0,0 +1,166 @@ +# Form Management in Aetherspace + +One of our overall goals is to enable the use of Zod schemas as the ultimate source of truth for your app's datastructure, validation and types. We've extended this concept to include form management as well. + +```tsx +import { useFormState } from 'aetherspace/forms' + +// Define a Zod schema for your form state (or use an existing one) +const formStateSchema = aetherSchema('SomeFormState', { + username: z.string().nonempty(), + email: z.string().email(), + password: z.string().min(8), + twoFactorCode: z.number().length(2), +}) + +// Create a set of form state utils to use in your components +const formState = useFormState({ + stateSchema: formStateSchema, + initialValues: { + username: '', + email: '', + password: '', + twoFactorCode: 0, + }, +}) +``` + +## Typed form state values + +`formState.values` is typed according to the Zod schema you provided as `stateSchema` + +```tsx +formState.values.username // string +formState.values.email // string +formState.values.password // string +formState.values.twoFactorCode // number +``` + +Alternatively, you can use `formState.getValue('some-key')` to get a specific value from the form state. The 'some-key' argument is any key in the Zod schema you provided as `stateSchema` and the available keys will he hinted by your IDE. + +```tsx +// Hinted keys: 'username' | 'email' | 'password' | 'twoFactorCode' +formState.getValue('username') // string +``` + +Updating the formState values can similarly be done in two ways: + +```tsx +// Update a single value in the form state by (hinted) key +formState.handleChange('username', 'codinsonn.dev') // OK +formState.handleChange('twoFactorCode', 'some-string') // ERROR +``` + +```tsx +// Update multiple values in the form state by passing an object with keys and values +formState.setValues({ + username: 'codinsonn.dev', // OK + email: 'thorr@codinsonn.dev', // OK + password: '*******', // OK + twoFactorCode: 'some-string', // ERROR: Type 'string' is not assignable to type 'number' +}) +``` + +Typescript and your IDE will help you out with the available keys and allowed values through hints and error markings if you try to set a value that doesn't match the Zod schema. + +## Form validation & error messages + +The form state also includes a `formState.errors` object that contains any errors that have been found in the form state when validating it against the Zod schema. + +To do that, you can call `formState.validate()` + +```tsx +// Validate the form state against the Zod schema +// true if the form state is valid +// false if the form state is invalid -> sets the formState.errors object +if (formState.validate()) { + // Do something... +} +``` + +```tsx +console.log(formState.errors) // { username: ['Required'], email: ['Invalid email'] } +``` + +```tsx +formState.hasError('username') // true +formState.getErrors('email') // ['Invalid email'] +``` + +You can specify custom error messages on each validation step when defining your Zod schema, and even translate them using libraries like `i18next`: + +```tsx +const formStateSchema = aetherSchema('SomeFormState', { + username: z + .string() + .nonempty(i18n._('We need a username to associate with your account')), + email: z + .string() + .email(i18n._('Please provide a valid email adress')), + password: z + .string() + .min(8, i18n._('Password must be at least 8 characters long')), + twoFactorCode: z + .number() + .length(2, i18n._('Two factor code must be 2 digits')), +}) +``` + +> When dealing with complex state with e.g. nested objects, keep in mind that calling `formState.validate()`, the error messages will be flattened and only contain the error messages for the top level keys. + +To update or clear the error messages manually, you can use the `formState.updateErrors()` functions: + +```tsx +// e.g. Clear all error messages by passing an empty object or empty arrays +formState.updateErrors({ + username: [], + email: [], + password: [], + twoFactorCode: [], +}) +``` + +## Integrating with React components + +You can integrate your formState for specific fields with React components by using the `formState.getInputProps()` function. This will return an object with the `value` and `onChange` props that you can pass to your component, as well as a `hasError` flag + `onBlur()` and `onFocus()` handlers. + +```tsx + +``` + +vs. manually assigning everything: + +```tsx + formState.handleChange('username', value)} + onBlur={() => formState.validate()} + hasError={formState.hasError('username')} +/> +``` + +## Resetting the form state + +You can reset the form state to its initial values by calling `formState.resetForm()`, e.g. + +```tsx + +``` + +## Other formState helpers + +`formState.isValid` - boolean, true if the form state is valid, false if it's invalid + +`formState.isUnsaved` - boolean, true if the form state has been changed from its initial values, false if it hasn't + +`formState.isDefaultState` - boolean, true if the form state is equal to its initial values, false if it isn't + +`formState.transformValues()` - function, will transform the form state values according to the optional `transformValues` function you provided to `useFormState()`, or simply apply the schema defaults if you didn't provide one. Handy for e.g. converting values to a different format before submitting them to an API. + +## Learn more about Aetherspace: + +- [Zod & Single Sources of Truth](/packages/@aetherspace/schemas/README.md) +- [GraphQL and Data-Fetching](/packages/@aetherspace/navigation/AetherPage/README.md) +- [Styling your form & screen components with Tailwind](/packages/@aetherspace/styles/README.md) diff --git a/packages/@aetherspace/navigation/AetherPage/README.md b/packages/@aetherspace/navigation/AetherPage/README.md index c31e3080..32f7ee07 100644 --- a/packages/@aetherspace/navigation/AetherPage/README.md +++ b/packages/@aetherspace/navigation/AetherPage/README.md @@ -1,6 +1,6 @@ # GraphQL, Data Resolvers & Fetching Route Data -As you may have guessed, the G in GREEN stack stands for GraphQL. It is an essential part of the stack, and is used to fetch data for all pages of the app you're building with Aetherspace. +As you probably know, the G in 'GREEN stack' stands for GraphQL. It is an essential part of the stack, and is used to fetch data for all pages of the app you're building with Aetherspace. --- @@ -37,7 +37,7 @@ On top of that, the way to create a GraphQL API in Aetherspace can also be used # Creating GraphQL Resolvers -### With Schemas and just functions™️ +### By combining zod based schemas and simple resolver functions As stated in the [Aetherspace Quickstart](/packages/@aetherspace/README.md), you can create a Data Resolver by: @@ -112,9 +112,9 @@ export const POST = makeNextRouteHandler(healthCheck) On top of that, the `healthCheck` function "bundle" we made by wrapping with `aetherResolver()` is still usable as a regular Javascript promise for use in other data resolvers as well. -### Easy mode -- Generating GraphQL Resolvers with the CLI +### Generating GraphQL Resolvers with the CLI (Recommended) -If you'd rather skip some of these manual steps, we have a handy turborepo generator in place: +If you'd rather skip some of these manual steps, we have a handy **turborepo generator** in place: ```shell yarn ats add-resolver @@ -233,7 +233,7 @@ export const HomeScreen = (props: AetherProps) => { But again, you may want to save some time by skipping the manual boilerplate entirely and use a generator instead: -## Generating GraphQL powered routes +## Generating GraphQL powered routes (Recommended) ```shell yarn ats add-route @@ -269,11 +269,31 @@ The turborepo route generator will ask you some questions, like which url you’ In the generated screen component file, you can then replace the boilerplate `healthCheck` graphql query with whatever data you need from the GraphQL explorer at [/api/graphql](http://localhost:3000/api/graphql) -## Limitations: Unions & Tuples +## Creating a "DataBridge" for Linking Routes to GraphQL Query Resolvers -Since GraphQL does not support Union and Tuple types out of the box, we have not yet added support for transforming your zod tuples and union fields into GraphQL types either. For now, these fields will just be ignored. +Like you saw in the example above, we use a `screenConfig` object to bundle the GraphQL query, variables, and data fetcher together. This is then used by the `useAetherRoute` hook to fetch the data for the screen. -If you can, try to avoid them in your resolver arguments and responses by going for a more flat or object based structure instead. +Ideally, these are extendable and composable, so that you can create a "DataBridge" between your GraphQL API and your routes. This is what the `createDataBridge()` function is for: + +> Note: **Any resolver generated with the CLI will already have a DataBridge in place.** + +```tsx +const screenConfig = createDataBridge({ + ...SomeResolverDataBridge, // <- Resolver DataBridge we're extending for our route + paramsSchema: ResumeScreenParams, + propsSchema: ResumeScreenProps, + graphqlQuery: someCustomQueryWithLessFields, // <- Override the GraphQL query if needed + backgroundColor: '#111827', +}) +``` + +> Further notes: **In the route generator, you can actually already select a resolver to use as a DataBridge for your route**. This will automatically generate the `createDataBridge()` call for you, and will also generate the `ResumeScreenParams` and `ResumeScreenProps` schemas for you. + +## Using Unions & Tuples + +Since **GraphQL does not support Union and Tuple types out of the box**, we have only added support for transforming your zod tuples and union fields into GraphQL JSON types. These will act as a sort of catch-all for types we don't yet know how to handle in a type-safe way. However, since zod is the extra barrier of validation, this should not really be a problem. + +If you really want a type-safe GraphQL schema as well, you can try to avoid them in your resolver arguments and responses by going for a more flat or object based structure instead. e.g. instead of: @@ -317,4 +337,5 @@ type SomeSchema { - [Single Sources of Truth for your Web & Mobile apps](/packages/@aetherspace/schemas/README.md) - [Universal Routing in Expo & Next.js with Aetherspace](/packages/@aetherspace/navigation/README.md) +- [Form State Management in Aetherspace with Zod](/packages/@aetherspace/forms/README.md) - [Automation based on Single Sources of Truth and the File System](/packages/@aetherspace/scripts/README.md) diff --git a/packages/@aetherspace/navigation/README.md b/packages/@aetherspace/navigation/README.md index df2df39b..24bfebed 100644 --- a/packages/@aetherspace/navigation/README.md +++ b/packages/@aetherspace/navigation/README.md @@ -1,8 +1,20 @@ # Universal Routing for Expo & Next.js -When combining React for Web and React-Native for Mobile, navigation has always been one of the hardest problems to solve. Luckily, with file-based routing in both Next.js, -and more recently, Expo-Router, we can provide an easy way of managing your routes: +When combining React for Web and React-Native for Mobile, navigation has always been one of the hardest problems to solve. Luckily, with file-based routing in both Next.js, -and more recently, Expo-Router, we can provide an easy way of managing your routes on the workspace level: -## Easy Mode — Using the Route Generator +```shell +/workspace/ +└── /screens/ # ➡️ Pages used in /routes/ +└── /routes/ # ➡️ Routing linked to expo & next.js app-dir using scripts + └── api/ # ➡️ Houses all REST API's, copied to app dirs using a script + └── about/ + └── index.tsx # ➡️ Will be available at '/about' in Expo + Next + └── blog/ + └── [slug]/ + └── index.tsx # ➡️ Will be available at '/blog/[slug]' in Expo + Next +``` + +## Using the Route Generator (Recommended) ```shell yarn ats add-route diff --git a/packages/@aetherspace/schemas/README.md b/packages/@aetherspace/schemas/README.md index 8c46e40f..84640147 100644 --- a/packages/@aetherspace/schemas/README.md +++ b/packages/@aetherspace/schemas/README.md @@ -359,4 +359,5 @@ const someSchema = aetherSchema('SomeSchema', { - Read the [official zod docs at zod.dev](https://zod.dev/) (or watch an [intro video](https://www.youtube.com/watch?v=L6BE-U3oy80)) - [Writing flexible data resolvers with Schemas](/packages/@aetherspace/navigation/AetherPage/README.md) +- [Form State Management in Aetherspace with Zod](/packages/@aetherspace/forms/README.md) - [Automation based on Schemas: Storybook & GraphQL](/packages/@aetherspace/scripts/README.md)