Skip to content

Commit

Permalink
Refactored form component, added dedicated provider and context methods
Browse files Browse the repository at this point in the history
  • Loading branch information
Matt Mangan committed Jan 12, 2024
1 parent 3908945 commit ea8466d
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 113 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
"chai-as-promised": "7.1.1",
"chromatic": "6.17.4",
"concurrently": "8.0.1",
"cypress": "^12.14.0",
"cypress": "12.14.0",
"cypress-vite": "1.4.0",
"eslint": "8.33.0",
"eslint-config-prettier": "8.6.0",
Expand Down
53 changes: 45 additions & 8 deletions src/dataset/domain/useCases/createDataset.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,52 @@
export interface CreateDatasetFormData {
export interface CreateDatasetFormFields {
createDatasetTitle: string
}

export class CreateDataset {
submitDataset = async (formData: CreateDatasetFormData): Promise<string> => {
console.log('Submitting dataset:', formData)
return Promise.resolve('Form submitted successfully!')
export interface FormValidationResult {
isValid: boolean
errors: Record<keyof CreateDatasetFormFields, string | undefined>
}

export interface FormValidationService {
validateForm: (fields: CreateDatasetFormFields) => FormValidationResult
}

export const formValidationService: FormValidationService = {
validateForm: (fields: CreateDatasetFormFields): FormValidationResult => {
const errors: Record<keyof CreateDatasetFormFields, string | undefined> = {
createDatasetTitle: undefined
}

if (!fields.createDatasetTitle) {
errors.createDatasetTitle = 'Name is required'
}

return {
isValid: Object.values(errors).every((error) => error === undefined),
errors
}
}
}

// formSubmissionService
export interface FormSubmissionService {
submitFormData: (fields: CreateDatasetFormFields) => Promise<void>
}
export const formSubmissionService: FormSubmissionService = {
// If there's an asynchronous operation, use `await` here.
submitFormData: async (fields: CreateDatasetFormFields): Promise<void> => {
// For example, if submitting data to a server:
console.log('Submitting form data:', fields)
const sendDataMock = (fields: CreateDatasetFormFields) => {
// Simulate a network delay, for example, 2 seconds
const delay = 2000

validateCreateDatasetFormData = (formData: CreateDatasetFormData): boolean => {
// Add validation logic here
return formData.createDatasetTitle.trim() !== ''
return new Promise((resolve) => {
setTimeout(() => {
resolve('Form Data Submitted: ' + JSON.stringify(fields))
}, delay)
})
}
await sendDataMock(fields)
}
}
73 changes: 23 additions & 50 deletions src/sections/create-dataset/CreateDatasetContext.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,30 @@
import React, { createContext, useContext, ReactNode } from 'react'
import CreateDatasetFormPresenter from './CreateDatasetForm'
import { useTranslation } from 'react-i18next'
import { CreateDataset, CreateDatasetFormData } from '../../dataset/domain/useCases/createDataset'
import styles from '/src/sections/dataset/Dataset.module.scss'
import { SeparationLine } from '../../components/layout/SeparationLine/SeparationLine'
import { createContext, useContext } from 'react'
import { CreateDatasetFormFields } from '../../dataset/domain/useCases/createDataset' // Importing the FormFields type

interface CreateDatasetFormProps {
submitDataset: (formData: CreateDatasetFormData) => Promise<string>
validateCreateDatasetFormData: (formData: CreateDatasetFormData) => boolean
// Define the context and its interface
interface FormContextInterface {
formState: CreateDatasetFormFields
updateFormState: (newState: CreateDatasetFormFields) => void
}

const DatasetContext = createContext<CreateDatasetFormProps>({} as CreateDatasetFormProps)
interface DatasetProviderProps {
children: ReactNode
// Define default values for the context
const defaultFormState: CreateDatasetFormFields = {
// Initialize with default values for your form fields
createDatasetTitle: ''
}
export const DatasetProvider: React.FC<DatasetProviderProps> = ({ children }) => {
const createDatasetUseCase = new CreateDataset()

return (
<DatasetContext.Provider
value={{
submitDataset: createDatasetUseCase.submitDataset,
validateCreateDatasetFormData: createDatasetUseCase.validateCreateDatasetFormData
}}>
{children}
</DatasetContext.Provider>
)
const defaultContext: FormContextInterface = {
formState: defaultFormState,
updateFormState: () => {
// This is a no-op function since the default context shouldn't update anything
}
}
// Hook for easy consumption of the context
export const useDataset = () => useContext(DatasetContext)

export function CreateDatasetContainer() {
const { t } = useTranslation('createDataset')
return (
<>
<article>
<header className={styles.header}>
<h1>{t('pageTitle')}</h1>
</header>
<SeparationLine />
<div className={styles.container}>
<CreateDatasetFormPresenter />
</div>
</article>
</>
)
}

export const DatasetCreateMaster: React.FC = () => {
return (
<DatasetProvider>
<CreateDatasetContainer />
</DatasetProvider>
)
export const FormContext = createContext<FormContextInterface>(defaultContext)
// Custom hook to use the form context
export const useFormContext = () => {
const context = useContext(FormContext)
if (!context) {
throw new Error('useFormContext must be used within a FormProvider')
}
return context
}
export default DatasetCreateMaster
17 changes: 15 additions & 2 deletions src/sections/create-dataset/CreateDatasetFactory.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import { CreateDatasetFormPresenter } from './CreateDatasetForm'
import { CreateDatasetProvider } from './CreateDatasetProvider'
import { formValidationService } from '../../dataset/domain/useCases/createDataset'
import { formSubmissionService } from '../../dataset/domain/useCases/createDataset'
import { ReactElement } from 'react'
import { DatasetCreateMaster } from './CreateDatasetContext'
export const CreateDatasetForm: React.FC = () => {
return (
<CreateDatasetProvider>
<CreateDatasetFormPresenter
formValidationService={formValidationService}
formSubmissionService={formSubmissionService}
/>
</CreateDatasetProvider>
)
}

export class CreateDatasetFactory {
static create(): ReactElement {
return <DatasetCreateMaster />
return <CreateDatasetForm />
}
}
93 changes: 59 additions & 34 deletions src/sections/create-dataset/CreateDatasetForm.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,79 @@
import React, { useState } from 'react'
import React, { FormEvent, ChangeEvent, useState } from 'react'
import { Alert, Button, Col, Form, Row } from '@iqss/dataverse-design-system'
import { useTranslation } from 'react-i18next'
import { RequiredFieldText } from '../../components/form/RequiredFieldText/RequiredFieldText'
import { SeparationLine } from '../../components/layout/SeparationLine/SeparationLine'
import { useDataset } from './CreateDatasetContext'
import { FormInputElement } from '@iqss/dataverse-design-system/src/lib/components/form/form-group/form-element/FormInput'
import { useFormContext } from './CreateDatasetContext'
import {
CreateDatasetFormFields,
FormValidationService,
FormValidationResult
} from '../../dataset/domain/useCases/createDataset'
import { FormSubmissionService } from '../../dataset/domain/useCases/createDataset'

const CreateDatasetFormPresenter: React.FC = () => {
type CreateDatasetFormProps = {
formValidationService: FormValidationService
formSubmissionService: FormSubmissionService
}

enum SubmissionStatus {
Unsubmitted,
Submitted,
Errored
}

export const CreateDatasetFormPresenter: React.FC<CreateDatasetFormProps> = ({
formValidationService,
formSubmissionService
}) => {
const { formState, updateFormState } = useFormContext()
const [submissionStatus, setSubmissionStatus] = React.useState<SubmissionStatus>(
SubmissionStatus.Unsubmitted
)
const [isSubmitting, setIsSubmitting] = React.useState<boolean>(false)
const [formErrors, setFormErrors] = useState<
Record<keyof CreateDatasetFormFields, string | undefined>
>({
createDatasetTitle: undefined
})
const { t } = useTranslation('createDataset')
const [formData, setFormData] = useState({ createDatasetTitle: '' })
const { submitDataset, validateCreateDatasetFormData } = useDataset()
const [isSubmitting, setIsSubmitting] = useState(false)
const [submitSuccess, setSubmitSuccess] = useState(false)

const handleCreateDatasetFieldChange = (event: React.ChangeEvent<FormInputElement>): void => {
const handleCreateDatasetFieldChange = (event: ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target
setFormData((formData) => ({
...formData,
[name]: value
}))
updateFormState({ ...formState, [name]: value })
}

const handleCreateDatasetSubmit = async (event: React.FormEvent) => {
const handleCreateDatasetSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
setSubmitSuccess(false)
if (validateCreateDatasetFormData(formData)) {
setIsSubmitting(true)
try {
await submitDataset(formData)
// setFormData({ createDatasetTitle: '') // Reset form fields
setSubmitSuccess(true)
} catch (error) {
console.error('Error during submission:', error)
// Optionally handle the error state here
} finally {
setIsSubmitting(false)
}
setIsSubmitting(true)
setSubmissionStatus(SubmissionStatus.Unsubmitted)

const validationResult: FormValidationResult = formValidationService.validateForm(formState)

if (validationResult.isValid) {
formSubmissionService
.submitFormData(formState)
.then(() => setSubmissionStatus(SubmissionStatus.Submitted))
.catch(() => setSubmissionStatus(SubmissionStatus.Errored))
.finally(() => setIsSubmitting(false))
} else {
console.error('Validation failed: Title is required.')
// Optionally handle the validation error state here
setFormErrors(validationResult.errors)
setSubmissionStatus(SubmissionStatus.Errored)
setIsSubmitting(false)
}
}

return (
<>
<RequiredFieldText />
{isSubmitting && <p>Submitting...</p>}
{submissionStatus === SubmissionStatus.Submitted && <p>Form submitted successfully!</p>}
{submissionStatus === SubmissionStatus.Errored && <p>Error: Submission failed.</p>}
<Form
onSubmit={() => {
void handleCreateDatasetSubmit
onSubmit={(event) => {
handleCreateDatasetSubmit(event)
}}
className={'create-dataset-form'}>
{submitSuccess && <div>Form Submitted!</div>}
<Row>
<Col md={9}>
<Form.Group controlId="createDatasetTitle" required>
Expand All @@ -64,18 +87,20 @@ const CreateDatasetFormPresenter: React.FC = () => {
withinMultipleFieldsGroup={false}
/>
</Form.Group>
{formErrors.createDatasetTitle && <span>{formErrors.createDatasetTitle}</span>}
</Col>
</Row>
<SeparationLine />
<Alert variant={'info'} customHeading={t('metadataTip.title')} dismissible={false}>
{t('metadataTip.content')}
</Alert>
<Button type="submit">{t('saveButton')}</Button>
<Button type="submit" disabled={isSubmitting}>
{t('saveButton')}
</Button>
<Button withSpacing variant="secondary">
{t('cancelButton')}
</Button>
</Form>
</>
)
}
export default CreateDatasetFormPresenter
38 changes: 38 additions & 0 deletions src/sections/create-dataset/CreateDatasetProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { PropsWithChildren, useState, ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import styles from '/src/sections/dataset/Dataset.module.scss'
import { SeparationLine } from '../../components/layout/SeparationLine/SeparationLine'
import { FormContext } from './CreateDatasetContext'
import { CreateDatasetFormFields } from '../../dataset/domain/useCases/createDataset'
// Add a type for the children prop
type FormProviderProps = {
children: ReactNode
}
// Context Provider Component
export const CreateDatasetProvider: React.FC<FormProviderProps> = ({
children
}: PropsWithChildren) => {
const [formState, setFormState] = useState<CreateDatasetFormFields>({
createDatasetTitle: ''
})
const { t } = useTranslation('createDataset')
const updateFormState = (newState: CreateDatasetFormFields) => {
setFormState((prevState) => ({ ...prevState, ...newState }))
}

return (
<>
<article>
<header className={styles.header}>
<h1>{t('pageTitle')}</h1>
</header>
<SeparationLine />
<div className={styles.container}>
<FormContext.Provider value={{ formState, updateFormState }}>
{children}
</FormContext.Provider>
</div>
</article>
</>
)
}
17 changes: 8 additions & 9 deletions src/stories/dataset/dataset-create/DatasetCreate.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import type { Meta, StoryObj } from '@storybook/react'
import { WithI18next } from '../../WithI18next'
import type { StoryObj, Meta } from '@storybook/react'
import { CreateDatasetForm } from '../../../sections/create-dataset/CreateDatasetFactory'
import { WithLayout } from '../../WithLayout'
import DatasetCreateMaster from '../../../sections/create-dataset/CreateDatasetContext'
import { WithI18next } from '../../WithI18next'

const meta: Meta<typeof DatasetCreateMaster> = {
const meta: Meta<typeof CreateDatasetForm> = {
title: 'Pages/Create Dataset',
component: DatasetCreateMaster,
component: CreateDatasetForm,
decorators: [WithI18next, WithLayout]
}

export default meta
type Story = StoryObj<typeof DatasetCreateMaster>
type Story = StoryObj<typeof CreateDatasetForm>

export const Default: Story = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
render: () => <DatasetCreateMaster />
decorators: [WithLayout, WithI18next],
render: () => <CreateDatasetForm />
}
Loading

0 comments on commit ea8466d

Please sign in to comment.