Skip to content

Commit

Permalink
refactor everything
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolaslopezj committed Jan 17, 2023
1 parent 5f4c52d commit 3467079
Show file tree
Hide file tree
Showing 12 changed files with 859 additions and 438 deletions.
4 changes: 0 additions & 4 deletions .eslintrc

This file was deleted.

52 changes: 52 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
module.exports = {
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'eslint-plugin-import', 'unused-imports', 'react'],
extends: ['plugin:@typescript-eslint/recommended'],
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
modules: true
}
},
rules: {
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/ban-types': 'off',
'func-names': 0,
'@typescript-eslint/no-namespace': 0,
'@typescript-eslint/explicit-module-boundary-types': 'off',
'import/extensions': [
'error',
'ignorePackages',
{
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never'
}
],
'no-restricted-imports': ['error', 'lodash'],
'unused-imports/no-unused-imports': 'error',
'react/jsx-key': ['error', {checkFragmentShorthand: true}]
},
env: {
browser: true,
node: true,
commonjs: true,
jest: true
},
settings: {
indent: ['error', 2],
'import/extensions': ['.js', '.jsx', '.ts', '.tsx'],
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx']
},
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx']
}
}
}
}
9 changes: 4 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,10 @@
"@types/react": "^17.0.37",
"@types/react-dom": "^17.0.11",
"@types/testing-library__jest-dom": "^5.14.2",
"eslint": "^8.4.1",
"eslint-config-orionsoft": "^2.2.0",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-react": "^7.27.1",
"eslint": "^8.31.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-react": "^7.32.0",
"eslint-plugin-unused-imports": "^2.0.0",
"jest": "27.4.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
Expand Down
17 changes: 12 additions & 5 deletions src/Array/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Field from '../Field'
import {ParentFieldNameContext} from '../Contexts'
import {FieldProps} from '../types'

export type propTypes = FieldProps & {
export type ArrayComponentProps = {
/**
* The error messages for the children fields. Used for object and array
*/
Expand All @@ -24,6 +24,11 @@ export type propTypes = FieldProps & {
*/
addLabel?: string

/**
* disable the add button
*/
disabled?: boolean

/**
* Show the add button
*/
Expand Down Expand Up @@ -69,7 +74,7 @@ export type propTypes = FieldProps & {
renderProps?: boolean
}

const defaultProps: Partial<propTypes> = {
const defaultProps: Partial<ArrayComponentProps> = {
addLabel: 'Add',
removeLabel: 'Remove',
errorMessages: {},
Expand All @@ -79,7 +84,9 @@ const defaultProps: Partial<propTypes> = {
renderProps: false
}

export default class ArrayComponent extends React.Component<propTypes> {
export default class ArrayComponent extends React.Component<
FieldProps<any[], ArrayComponentProps>
> {
static defaultProps = defaultProps

addItem(itemValue = {}) {
Expand Down Expand Up @@ -133,10 +140,10 @@ export default class ArrayComponent extends React.Component<propTypes> {
)
}

renderChildrenItemWithContext({index, children}) {
renderChildrenItemWithContext({index, children}: {index: number; children: any}) {
return (
<ParentFieldNameContext.Provider key={index} value={this.props.fieldName}>
<Field fieldName={`${index}`} type={this.getObjectField()}>
<Field fieldName={String(index)} type={this.getObjectField()}>
{this.props.renderProps ? children(index) : children}
</Field>
</ParentFieldNameContext.Provider>
Expand Down
86 changes: 82 additions & 4 deletions src/Field/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import React, {useState} from 'react'
import Form from '../Form'
import Field from '../Field'
import {render} from '@testing-library/react'
import {act, fireEvent, render, screen} from '@testing-library/react'
import '@testing-library/jest-dom'
import {FieldProps} from '../types'

Expand Down Expand Up @@ -47,12 +47,27 @@ test('should pass parent value', () => {
expect(checked).toBe(true)
})

test('should inherit field type props', () => {
interface DummyProps {
name: string
}
function DummyInput(props: FieldProps<string, DummyProps>) {
return <div></div>
}

render(
<Form state={{hello: 'world'}}>
<Field fieldName="hello" type={DummyInput} name="ss2ss" />
</Form>
)
})

test('should be able to add any prop to the field', () => {
let checked = false
function DummyInput(props: FieldProps) {
function DummyInput(props: FieldProps<any, {passingProp: number}>) {
checked = true
expect(props.parentValue).toEqual({hello: 'world'})
return null
return <div>dummy</div>
}

render(
Expand All @@ -63,3 +78,66 @@ test('should be able to add any prop to the field', () => {

expect(checked).toBe(true)
})

test('should allow using nested field with onChange', async () => {
function HelloInput(props: FieldProps) {
return (
<>
<button
onClick={() => {
props.onChange('no')
}}>
setno
</button>
{props.fieldName}: {props.value}
</>
)
}

function ItemsInput(props: FieldProps) {
return (
<>
<button
onClick={() => {
props.onChange(oldVal => {
return [...oldVal, {hello: oldVal.length}]
})
}}>
add
</button>
{props.value.map((item, index: number) => {
return <Field key={index} fieldName={String(`${index}.hello`)} type={HelloInput} />
})}
</>
)
}

let testValue = null
function Test() {
const [state, setState] = useState({items: []})
testValue = state

return (
<Form state={state} onChange={setState}>
<Field fieldName="items" type={ItemsInput} />
</Form>
)
}

render(<Test />)
await act(async () => {
fireEvent.click(screen.getByText('add'))
})

await act(async () => {
fireEvent.click(screen.getByText('setno'))
})

await act(async () => {
fireEvent.click(screen.getByText('add'))
fireEvent.click(screen.getByText('add'))
fireEvent.click(screen.getByText('add'))
})

expect(testValue).toEqual({items: [{hello: 'no'}, {hello: 1}, {hello: 2}, {hello: 3}]})
})
140 changes: 52 additions & 88 deletions src/Field/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react'
import React, {forwardRef, JSXElementConstructor, useContext, useMemo} from 'react'
import omit from 'lodash/omit'
import keys from 'lodash/keys'
import get from 'lodash/get'
import {
ValueContext,
Expand All @@ -9,105 +8,70 @@ import {
ParentFieldNameContext
} from '../Contexts'
import {fieldPropsKeys, FormFieldProps} from '../types'
import union from 'lodash/union'

export default class Field extends React.Component<FormFieldProps> {
input: any
// Redecalare forwardRef
declare module 'react' {
function forwardRef<T, P = {}>(
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null
): (props: P & React.RefAttributes<T>) => React.ReactElement | null
}

function FieldInner<TFieldType extends JSXElementConstructor<any>>(
props: FormFieldProps<TFieldType>,
ref: any
) {
const parentValue = useContext(ValueContext)
const errorMessages = useContext(ErrorMessagesContext) || {}
const onChange = useContext(OnChangeContext)
const parentFieldName = useContext(ParentFieldNameContext)

getFieldName(parentFieldName) {
const fieldName = useMemo(() => {
if (parentFieldName) {
if (this.props.fieldName) {
return `${parentFieldName}.${this.props.fieldName}`
if (props.fieldName) {
return `${parentFieldName}.${props.fieldName}`
} else {
return parentFieldName
}
} else {
return this.props.fieldName
}
}

focus = () => {
if (!this.input.focus) {
throw new Error("Field doesn't has a focus method")
return props.fieldName
}
this.input.focus()
}
}, [parentFieldName, props.fieldName])

getComponent() {
return this.props.type
}
const errorMessage = useMemo(() => {
return props.errorMessage || errorMessages[fieldName] || get(errorMessages, fieldName)
}, [fieldName, props.errorMessage, errorMessages])

getErrorMessage(errorMessages, parentFieldName) {
return (
this.props.errorMessage ||
errorMessages[this.getFieldName(parentFieldName)] ||
get(errorMessages, this.getFieldName(parentFieldName))
)
}
const childProps = useMemo(() => {
const propOptions = omit(props, ['fieldName', 'type', 'errorMessage']) as any
const allowedKeys = [...fieldPropsKeys, 'type']
const passProps = omit(propOptions, allowedKeys)

getChildProps({value, parentFieldName, onChange, errorMessages}) {
/**
* This gets the props that are defined in the propTypes of the registered component.
*/
const fieldComponent = this.getComponent()
const propOptions = omit(this.props, ['fieldName', 'type', 'errorMessage'])
const allowedKeys = union(keys({...fieldComponent.propTypes}), fieldPropsKeys)

/**
* Options that are not registered in the propTypes are passed also
* in the passProps object
*/
allowedKeys.push('type')
const notDefinedOptions = omit(propOptions, allowedKeys)

const props = {
value: get(value || {}, this.props.fieldName),
parentValue: value || {},
onChange: newValue => onChange(this.getFieldName(parentFieldName), newValue),
errorMessage: this.getErrorMessage(errorMessages || {}, parentFieldName),
fieldName: this.getFieldName(parentFieldName),
passProps: notDefinedOptions,
...propOptions
return {
...propOptions,
value: get(parentValue || {}, props.fieldName),
parentValue: parentValue || {},
onChange: newValue => {
return onChange(fieldName, newValue)
},
errorMessage,
fieldName,
passProps
}
}, [props, parentValue, fieldName, errorMessage])

return props
}
const Component = props.type

renderComponent(info) {
const Component = this.getComponent()
const props = this.getChildProps(info)
const ref = Component.prototype.render ? {ref: input => (this.input = input)} : {}
return (
<ValueContext.Provider value={props.value}>
<Component {...ref} {...props} />
</ValueContext.Provider>
)
}
const componentRef = Component.prototype.render ? ref : null

render() {
return (
<ValueContext.Consumer>
{value => (
<ErrorMessagesContext.Consumer>
{errorMessages => (
<OnChangeContext.Consumer>
{onChange => (
<ParentFieldNameContext.Consumer>
{parentFieldName =>
this.renderComponent({
value,
parentFieldName,
onChange,
errorMessages
})
}
</ParentFieldNameContext.Consumer>
)}
</OnChangeContext.Consumer>
)}
</ErrorMessagesContext.Consumer>
)}
</ValueContext.Consumer>
)
}
return (
<ValueContext.Provider value={childProps.value}>
<ParentFieldNameContext.Provider value={childProps.fieldName}>
<Component {...childProps} ref={componentRef} />
</ParentFieldNameContext.Provider>
</ValueContext.Provider>
)
}

const Field = forwardRef(FieldInner)

export default Field
Loading

0 comments on commit 3467079

Please sign in to comment.