Skip to content

Commit

Permalink
feat(sectionedForm): initial SectionedForm architecture
Browse files Browse the repository at this point in the history
  • Loading branch information
Birkbjo committed Nov 4, 2024
1 parent 3607f67 commit 3f03cdb
Show file tree
Hide file tree
Showing 12 changed files with 272 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/lib/form/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export { required } from './validators'
export { validate, createFormValidate } from './validate'
export { useOnSubmitEdit, useOnSubmitNew } from './useOnSubmit'
export { modelFormSchemas } from './modelFormSchemas'
export * from './sectionedForm'
14 changes: 14 additions & 0 deletions src/lib/form/sectionedForm/SectionedFormBase.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react'
import { SectionedFormProvider } from './SectionedFormContext'

type SectionedFormProps = {
name: string
children: React.ReactNode
}
export const SectionedFormBase = ({ name, children }: SectionedFormProps) => {
return (
<SectionedFormProvider initialValue={{ name }}>
{children}
</SectionedFormProvider>
)
}
20 changes: 20 additions & 0 deletions src/lib/form/sectionedForm/SectionedFormContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React, { createContext, useState } from 'react'
import { createFormStore, FormProps, FormStore } from './formStore'

export const SectionedFormContext = createContext<FormStore | null>(null)

export const SectionedFormProvider = ({
children,
initialValue,
}: {
initialValue: Partial<FormProps>
children: React.ReactNode
}) => {
const [store] = useState(() => createFormStore(initialValue))

return (
<SectionedFormContext.Provider value={store}>
{children}
</SectionedFormContext.Provider>
)
}
21 changes: 21 additions & 0 deletions src/lib/form/sectionedForm/SectionedFormField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { useEffect } from 'react'
import { useRegisterField } from './useRegister'

type SectionedFormField = {
label: string
name: string
children: React.ReactNode
}
export const SectionFormField = ({
label,
name,
children,
}: SectionedFormField) => {
const register = useRegisterField()

useEffect(() => {
register({ name, label })
}, [register, name, label])

return children
}
24 changes: 24 additions & 0 deletions src/lib/form/sectionedForm/SectionedFormSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React, { useEffect, useState } from 'react'
import { SectionedFormSectionProvider } from './SectionedFormSectionContext'
import { createSectionStore } from './sectionStore'
import { SectionedFormSectionContext } from './SectionedFormSectionContext'

Check failure on line 4 in src/lib/form/sectionedForm/SectionedFormSection.tsx

View workflow job for this annotation

GitHub Actions / lint

`./SectionedFormSectionContext` import should occur before import of `./sectionStore`
import { useRegisterFormSection } from './useRegister'

Check failure on line 5 in src/lib/form/sectionedForm/SectionedFormSection.tsx

View workflow job for this annotation

GitHub Actions / build

Module '"./useRegister"' has no exported member 'useRegisterFormSection'.

type SectionFormSectionProps = {
label: string
name: string
children: React.ReactNode
}
export const SectionedFormSection = ({
label,
name,
children,
}: SectionFormSectionProps) => {
return (
<SectionedFormSectionProvider
initialValue={{ section: { name, label } }}
>
{children}
</SectionedFormSectionProvider>
)
}
36 changes: 36 additions & 0 deletions src/lib/form/sectionedForm/SectionedFormSectionContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { createContext, useEffect, useState } from 'react'
import { useStore } from 'zustand'
import { SectionedFormContext } from './SectionedFormContext'
import { createSectionStore, SectionProps, SectionStore } from './sectionStore'

export const SectionedFormSectionContext = createContext<SectionStore | null>(
null
)

export const SectionedFormSectionProvider = ({
children,
initialValue,
}: {
initialValue: SectionProps
children: React.ReactNode
}) => {
const [store] = useState(() => createSectionStore(initialValue))
const formContext = React.useContext(SectionedFormContext)
if (!formContext) {
throw new Error(
'SectionedFormSectionProvider must be wrapped in a SectionFormSectionProvider'
)
}

const addSectionToForm = useStore(formContext, (state) => state.addSection)

useEffect(() => {
addSectionToForm(store.getState().section)
}, [store, addSectionToForm])

return (
<SectionedFormSectionContext.Provider value={store}>
{children}
</SectionedFormSectionContext.Provider>
)
}
73 changes: 73 additions & 0 deletions src/lib/form/sectionedForm/formStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { createStore } from 'zustand'
import { devtools } from 'zustand/middleware'
import { uniqueBy } from '../../../lib/utils'
import { SectionedFormSection, SectionFormField } from './types'

type SectionIdentifier = string | SectionedFormSection

export interface FormProps {
name: string
sections: SectionedFormSection[]
sectionFields: Map<string, SectionFormField[]>
}

export interface FormState extends FormProps {
addSection: (section: SectionedFormSection) => void
addField: (
section: string | SectionedFormSection,
field: SectionFormField
) => void
getFieldsForSection: (section: SectionIdentifier) => SectionFormField[]
getSectionsForField: (
field: string | SectionFormField
) => SectionedFormSection[] | undefined
}

export type FormStore = ReturnType<typeof createFormStore>

export const createFormStore = (initialProps: Partial<FormState>) =>
createStore<FormState>()(
devtools((set, get) => ({
name: '',
sections: [],
sectionFields: new Map(),
...initialProps,
addSection: (section: SectionedFormSection) => {
const prevSections = get().sections
set({ sections: prevSections.concat(section) })
},
addField: (section, field) => {
const sectionName = resolveSectionName(section)
const prevFieldsMap = get().sectionFields
const newFields = uniqueBy(
prevFieldsMap.get(sectionName)?.concat(field) || [field],
(field) => field.name
)

const sectionFields = new Map(prevFieldsMap).set(
sectionName,
newFields
)
set({ sectionFields })
},
getSections: () => get().sections,
getFieldsForSection: (section: SectionIdentifier) => {
const sectionName = resolveSectionName(section)
return get().sectionFields.get(sectionName) || []
},
getSectionsForField: (field: string | SectionFormField) => {
const fieldName = typeof field === 'string' ? field : field.name
const fieldsBySection = get().sectionFields
for (const [section, fields] of fieldsBySection.entries()) {
if (fields.find((f) => f.name === fieldName)) {
return get().sections.filter((s) => s.name === section)
}
}
return undefined
},
}))
)

const resolveSectionName = (section: SectionIdentifier) => {
return typeof section === 'string' ? section : section.name
}
4 changes: 4 additions & 0 deletions src/lib/form/sectionedForm/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { SectionFormField } from './SectionedFormField'
export { SectionedFormSection } from './SectionedFormSection'
export { SectionedFormBase } from './SectionedFormBase'
export { useSectionedFormState } from './useSectionedFormState'
26 changes: 26 additions & 0 deletions src/lib/form/sectionedForm/sectionStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { create, createStore } from 'zustand'
import { devtools } from 'zustand/middleware'
import { SectionedFormSection, SectionFormField } from './types'

export interface SectionProps {
section: SectionedFormSection
}

export interface SectionState extends SectionProps {
setSection: (section: SectionedFormSection) => void
getSection: () => SectionedFormSection
}
export type SectionStore = ReturnType<typeof createSectionStore>

export const createSectionStore = (initialProps: SectionProps) =>
createStore<SectionState>()(
devtools((set, get) => ({
...initialProps,
setSection: (section: SectionedFormSection) => {
set({ section })
},
getSection: () => {
return get().section
},
}))
)
9 changes: 9 additions & 0 deletions src/lib/form/sectionedForm/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type SectionedFormSection = {
name: string
label: string
}

export type SectionFormField = {
name: string
label: string
}
28 changes: 28 additions & 0 deletions src/lib/form/sectionedForm/useRegister.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { useCallback } from 'react'
import { useStore } from 'zustand'
import { SectionedFormContext } from './SectionedFormContext'
import { SectionedFormSectionContext } from './SectionedFormSectionContext'
import { SectionFormField } from './types'

export const useRegisterField = () => {
const sectionContext = React.useContext(SectionedFormSectionContext)!
const formContext = React.useContext(SectionedFormContext)!

const currentSection = useStore(sectionContext, (state) =>
state.getSection()
)

const addFieldToForm = useStore(formContext, (state) => state.addField)

return useCallback(
(field: SectionFormField) => {
if (currentSection) {
addFieldToForm(currentSection, field)
} else {
console.error(`Tried to register field ${field.name} in section, but no section is set.
Make sure to wrap fields in a SectionedFormSection component`)
}
},
[addFieldToForm, currentSection]
)
}
16 changes: 16 additions & 0 deletions src/lib/form/sectionedForm/useSectionedFormState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useContext } from 'react'
import { useStore } from 'zustand'
import { FormState } from './formStore'
import { SectionedFormContext } from './SectionedFormContext'

export function useSectionedFormState(): FormState
export function useSectionedFormState<T>(selector: (state: FormState) => T): T
export function useSectionedFormState<T>(selector?: (state: FormState) => T) {
const formStore = useContext(SectionedFormContext)
if (!formStore) {
throw new Error(
'useFormState must be used within a SectionedFormProvider'
)
}
return useStore(formStore, selector!)
}

0 comments on commit 3f03cdb

Please sign in to comment.