Skip to content

Commit

Permalink
DEV-101 MVP/ME Migrate to hooks based form solution (#25)
Browse files Browse the repository at this point in the history
* typescriptise .storybook

* wip

* wip

* wip

* form validator tests added

* small changes

* testing

* wip

* wip

* wip

* wip

* wip

* better time validation in the story

* a bit of cleanup

* some generic updates

* wip

* DEV-101 wip

* add generic source and value parameters

* error handling added

* small fix

* wip

* small re-facto

* remotdev & small refacto

* form cleanup

* remove unused tsconfig.node.json

* DEV-101

* DEV-101

* fix comments
  • Loading branch information
iz-podpolja authored Sep 14, 2022
1 parent 7ddf222 commit d361bd1
Show file tree
Hide file tree
Showing 25 changed files with 1,202 additions and 72 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ module.exports = {
globals: {},
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
project: ['./tsconfig.base.json', './tsconfig.tsnode.json'],
files: ['src/**/*', 'config/**/*'],
extraFileExtensions: ['.md', '.mdx'],
},
root: true,
Expand Down
29 changes: 6 additions & 23 deletions .storybook/main.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,6 @@
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-jest',
'@storybook/addon-a11y', // TODO: vite fix https://github.com/storybookjs/storybook/pull/17997
'storybook-addon-designs',
],
staticDirs: ['../public'],
framework: '@storybook/react',
core: {
builder: '@storybook/builder-vite',
},
async viteFinal(config, { configType }) {
console.log('env', process.env.BASE_URL, process.env.STORYBOOK_FIGMA_ACCESS_TOKEN)
config.base = process.env.BASE_URL || config.base

// return the customized config
return config
},
}
const register = require('ts-node-register')
register({
target: 'node16',
project: 'tsconfig.tsnode.json',
})
module.exports = require('./main.ts')
29 changes: 29 additions & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { StorybookConfig, Options } from '@storybook/core-common'
import { UserConfig } from 'vite'
interface ExtendedConfig extends StorybookConfig {
viteFinal?: (config: UserConfig, options: Options) => Promise<UserConfig>
}
const config: ExtendedConfig = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
'@storybook/addon-jest',
'@storybook/addon-a11y', // TODO: vite fix https://github.com/storybookjs/storybook/pull/17997
'storybook-addon-designs',
],
framework: '@storybook/react',
core: {
builder: '@storybook/builder-vite',
},
async viteFinal(config) {
console.log('env', process.env.BASE_URL, process.env.STORYBOOK_FIGMA_ACCESS_TOKEN)
config.base = process.env.BASE_URL || config.base

// return the customized config
return config
},
}

export default config
9 changes: 5 additions & 4 deletions .storybook/preview.js → .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { withDesign } from 'storybook-addon-designs'
import { withThemeProvider } from '../src/helpers'
import AVAILABLE_THEMES from '../src/lib/shared/themes'
import AvailableThemes from '../src/lib/shared/themes'
import { Parameters } from '@storybook/addons'
import order from './order.json'
export const parameters = {
export const parameters: Parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
Expand All @@ -21,9 +22,9 @@ export const globalTypes = {
theme: {
name: 'Theme',
description: 'Switch theme for preview',
defaultValue: AVAILABLE_THEMES[0].key,
defaultValue: AvailableThemes[0].key,
toolbar: {
items: AVAILABLE_THEMES.map((theme) => {
items: AvailableThemes.map((theme) => {
return {
value: theme.key,
title: theme.title,
Expand Down
2 changes: 1 addition & 1 deletion figma/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// eslint-disable @typescript-eslint/no-explicit-any
import { Node } from 'figma-api'
export function rgbToHex(r: number, g: number, b: number) {
const color = '#' + ((1 << 24) + ((r * 255) << 16) + ((g * 255) << 8) + b * 255).toString(16).slice(1)
Expand Down
13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,16 @@
"prepare": "husky install"
},
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
"classnames": "^2.3.1"
},
"peerDependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
"rxjs": "^7.5.5",
"react-dom": "^17.0.2",
"remotedev": "^0.2.9"
},
"devDependencies": {
"remotedev": "^0.2.9",
"@babel/core": "^7.17.9",
"@storybook/addon-a11y": "^6.4.22",
"@storybook/addon-actions": "^6.4.21",
Expand All @@ -58,6 +60,7 @@
"@swc/jest": "^0.2.21",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.0",
"@types/jest": "^27.5.1",
"@types/prettier": "^2.6.1",
"@types/react": "^17.0.10",
Expand Down Expand Up @@ -88,6 +91,9 @@
"lint-staged": ">=10",
"postcss-apply": "^0.12.0",
"postcss-nesting": "^10.1.7",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"rxjs": "^7.5.5",
"storybook-addon-designs": "^6.2.1",
"style-dictionary": "^3.7.0",
"stylelint": "^14.8.1",
Expand All @@ -97,6 +103,7 @@
"through2": "^4.0.2",
"ts-jest": "^28.0.2",
"ts-node": "^10.7.0",
"ts-node-register": "^1.0.0",
"typescript": "^4.5.4",
"vite": "^2.9.0",
"vite-plugin-dts": "^1.0.5",
Expand Down
38 changes: 19 additions & 19 deletions src/common.d.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
declare module '*.svg' {
const content: any
export default content
}
declare module '*.jpg' {
const content: any
export default content
}
declare module '*.png' {
const content: any
export default content
}
declare module '*.css' {
interface IClassNames {
[className: string]: string
}
const classNames: IClassNames
export = classNames
}
// declare module '*.svg' {
// const content: any
// export default content
// }
// declare module '*.jpg' {
// const content: any
// export default content
// }
// declare module '*.png' {
// const content: any
// export default content
// }
// declare module '*.css' {
// interface IClassNames {
// [className: string]: string
// }
// const classNames: IClassNames
// export = classNames
// }
19 changes: 14 additions & 5 deletions src/helpers/theme_provider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ReactFramework, StoryContext } from '@storybook/react'
import React, { ReactNode, useState } from 'react'
import '../../build/css/globals.css'
import tokens from '../../config/tokens.json'
import AVAILABLE_THEMES from '../lib/shared/themes'
const ThemeKeys = AVAILABLE_THEMES.map((theme) => theme.key)
type Theme = typeof ThemeKeys[number]
Expand All @@ -9,20 +10,28 @@ type ThemeProviderProps = {
theme: Theme
children: ReactNode
}
type Tokens = typeof tokens
export type ThemeProviderContext = {
theme?: Theme
tokens?: {
global: Tokens['global']
theme: Tokens[Exclude<keyof Tokens, 'global'>]
}
}

export const ThemeContext = React.createContext<Theme>('org')
export const ThemeContext = React.createContext<ThemeProviderContext>({})

export const ThemeProvider = (props: ThemeProviderProps) => {
export const ThemeProvider = ({ theme, children }: ThemeProviderProps) => {
const [themeVars, setThemeVars] = useState('')

import(`../../build/css/themes/${props.theme}.css`).then((styles) => {
import(`../../build/css/themes/${theme}.css`).then((styles) => {
setThemeVars(styles.default)
})

return (
<ThemeContext.Provider value={props.theme}>
<ThemeContext.Provider value={{ theme, tokens: { global: tokens.global, theme: tokens[theme] } }}>
<style id="theme-vars">{themeVars}</style>
{props.children}
{children}
</ThemeContext.Provider>
)
}
Expand Down
97 changes: 97 additions & 0 deletions src/lib/shared/form/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { HTMLInputTypeAttribute } from 'react'
import { catchError, debounceTime, delay, filter, from, mergeMap, Observable, of, switchMap } from 'rxjs'
import { createActionCreator, isActionOf } from '../store'
import { ActionType, Effect } from '../store/types'
import { Values, UseFormProps, FormState } from './types'

const ActionTypes = {
Submit: 'Form/Submit',
SubmitSuccess: 'Form/SubmitSuccess',
SubmitError: 'Form/SubmitError',
Validate: 'Form/Validate',
ValidateSuccess: 'Form/ValidateSuccess',
ValidateError: 'Form/ValidateError',
Reset: 'Form/Reset',
SetValue: 'Form/SetValue',
SetValues: 'Form/SetValues',
SetTouched: 'Form/SetTouched',
RegisterField: 'Form/RegisterField',
UnregisterField: 'Form/UnregisterField',
} as const

export class ActionFactory<T extends Values> {
Submit = createActionCreator(ActionTypes.Submit)<void>()
SubmitSuccess = createActionCreator(ActionTypes.SubmitSuccess)<T>()

SubmitError = createActionCreator(ActionTypes.SubmitError)<Error>()

Validate = createActionCreator(ActionTypes.Validate)<void>()

ValidateSuccess = createActionCreator(ActionTypes.ValidateSuccess)<{ [key in keyof T]?: string }>()

ValidateError = createActionCreator(ActionTypes.ValidateError)<Error>()

Reset = createActionCreator(ActionTypes.Reset)<void>()

SetValue = createActionCreator(ActionTypes.SetValue)<{ key: keyof T; value: T[keyof T]; internal?: boolean }>()

SetValues = createActionCreator(ActionTypes.SetValues)<Partial<T>>()

SetTouched = createActionCreator(ActionTypes.SetTouched)<{ key: keyof T; touched: boolean }>()

RegisterField = createActionCreator(ActionTypes.RegisterField)<{
key: keyof T
removeValueOnUnmount?: boolean
type?: HTMLInputTypeAttribute
}>()
UnregisterField = createActionCreator(ActionTypes.UnregisterField)<{ key: keyof T; type?: HTMLInputTypeAttribute }>()
}

export type FormActions<T extends Values> = ActionType<ActionFactory<T>[keyof ActionFactory<T>]>
export type ActionDispatch<T extends Values> = (value: FormActions<T>) => void

export type GetFormEffectsProps<T extends Values> = Omit<UseFormProps<T>, 'onValidate'> & {
onValidate: (values: T) => Observable<{ [key in keyof T]?: string | undefined }>
}
export const getFormEffects = <T extends Values>(
actions: ActionFactory<T>,
propsRef: React.RefObject<GetFormEffectsProps<T>>
): Array<Effect<FormState<T>, FormActions<T>>> => [
(action$) =>
action$.pipe(
filter(() => typeof propsRef.current?.onSubmit === 'function'),
isActionOf(actions.Submit),
switchMap(([_, __, { values }]) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return from(propsRef.current!.onSubmit!(values)).pipe(
mergeMap((values) => of(actions.SubmitSuccess(values))),
catchError((err) => of(actions.SubmitError(err)))
)
})
),
(action$) =>
action$.pipe(
isActionOf(actions.Validate),
switchMap(([_, __, { values }]) => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return from(propsRef.current!.onValidate!(values)).pipe(
mergeMap((values) => of(actions.ValidateSuccess(values))),
catchError((err) => of(actions.ValidateError(err)))
)
})
),
(action$) =>
action$.pipe(
isActionOf(actions.SetValue),
debounceTime(33),
delay(33),
switchMap((_) => of(actions.Validate(undefined)))
),
(action$) =>
action$.pipe(
isActionOf(actions.RegisterField),
debounceTime(33),
delay(33),
switchMap((_) => of(actions.Validate(undefined)))
),
]
Loading

0 comments on commit d361bd1

Please sign in to comment.