Skip to content

Commit

Permalink
feat(react): add NricField component (#2715)
Browse files Browse the repository at this point in the history
  • Loading branch information
karrui authored Sep 7, 2021
1 parent d0ee29e commit 6a0fbb1
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 0 deletions.
100 changes: 100 additions & 0 deletions frontend/src/templates/Field/Nric/NricField.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { useEffect, useState } from 'react'
import { FormProvider, useForm } from 'react-hook-form'
import { Text } from '@chakra-ui/react'
import { Meta, Story } from '@storybook/react'

import { BasicField } from '~shared/types/field'

import Button from '~components/Button'

import {
NricField as NricFieldComponent,
NricFieldProps,
NricFieldSchema,
} from './NricField'

export default {
title: 'Templates/Field/NricField',
component: NricFieldComponent,
decorators: [],
parameters: {
docs: {
// Required in this story due to react-hook-form conflicting with
// Storybook somehow.
// See https://github.com/storybookjs/storybook/issues/12747.
source: {
type: 'code',
},
},
},
} as Meta

const baseSchema: NricFieldSchema = {
title: 'NRIC/FIN',
description: 'Lorem ipsum what is your NRIC',
required: true,
disabled: false,
fieldType: BasicField.Nric,
_id: '611b94dfbb9e300012f702a7',
}

interface StoryNricFieldProps extends NricFieldProps {
defaultValue?: string
}

const Template: Story<StoryNricFieldProps> = ({ defaultValue, ...args }) => {
const formMethods = useForm({
defaultValues: {
[args.schema._id]: defaultValue,
},
})

const [submitValues, setSubmitValues] = useState<string>()

const onSubmit = (values: Record<string, string>) => {
setSubmitValues(values[args.schema._id] || 'Nothing was selected')
}

useEffect(() => {
formMethods.trigger()
}, [])

return (
<FormProvider {...formMethods}>
<form onSubmit={formMethods.handleSubmit(onSubmit)} noValidate>
<NricFieldComponent {...args} />
<Button
mt="1rem"
type="submit"
isLoading={formMethods.formState.isSubmitting}
loadingText="Submitting"
>
Submit
</Button>
{submitValues && <Text>You have submitted: {submitValues}</Text>}
</form>
</FormProvider>
)
}

export const ValidationRequired = Template.bind({})
ValidationRequired.args = {
schema: baseSchema,
}

export const ValidationOptional = Template.bind({})
ValidationOptional.args = {
schema: { ...baseSchema, required: false },
}

export const ValidationInvalidNric = Template.bind({})
ValidationInvalidNric.args = {
schema: baseSchema,
defaultValue: 'S0000002Z',
}

export const ValidationValidNric = Template.bind({})
ValidationValidNric.args = {
schema: baseSchema,
defaultValue: 'S0000001I',
}
110 changes: 110 additions & 0 deletions frontend/src/templates/Field/Nric/NricField.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { composeStories } from '@storybook/testing-react'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

import { REQUIRED_ERROR } from '~constants/validation'

import * as stories from './NricField.stories'

const { ValidationRequired, ValidationOptional } = composeStories(stories)

describe('validation required', () => {
it('renders error when field is not filled before submitting', async () => {
// Arrange
render(<ValidationRequired />)
const submitButton = screen.getByText('Submit')

// Act
userEvent.click(submitButton)
await waitFor(() => submitButton.textContent !== 'Submitting')

// Assert
// Should show error message.
const error = screen.getByText(REQUIRED_ERROR)
expect(error).not.toBeNull()
})

it('renders success when field has valid NRIC when submitted', async () => {
// Arrange
const schema = ValidationRequired.args?.schema
render(<ValidationRequired />)
const input = screen.getByLabelText(schema!.title) as HTMLInputElement
const submitButton = screen.getByText('Submit')

expect(input.value).toBe('')

// Act
// Valid NRIC
userEvent.type(input, 'S0000002G')
userEvent.click(submitButton)
await waitFor(() => submitButton.textContent !== 'Submitting')

// Assert
// Should show success message.
const success = screen.getByText('You have submitted: S0000002G')
expect(success).not.toBeNull()
const error = screen.queryByText('Please fill in required field')
expect(error).toBeNull()
})
})

describe('validation optional', () => {
it('renders success even when field is empty before submitting', async () => {
// Arrange
render(<ValidationOptional />)
const submitButton = screen.getByText('Submit')

// Act
userEvent.click(submitButton)
await waitFor(() => submitButton.textContent !== 'Submitting')

// Assert
// Should show success message.
const success = screen.getByText('You have submitted: Nothing was selected')
expect(success).not.toBeNull()
})

it('renders success when field has valid NRIC when submitted', async () => {
// Arrange
const schema = ValidationOptional.args?.schema
render(<ValidationOptional />)
const input = screen.getByLabelText(schema!.title) as HTMLInputElement
const submitButton = screen.getByText('Submit')

expect(input.value).toBe('')

// Act
userEvent.type(input, 'S0000001I')
userEvent.click(submitButton)
await waitFor(() => submitButton.textContent !== 'Submitting')

// Assert
// Should show success message.
const success = screen.getByText('You have submitted: S0000001I')
expect(success).not.toBeNull()
const error = screen.queryByText(REQUIRED_ERROR)
expect(error).toBeNull()
})
})

describe('NRIC validation', () => {
it('renders error when invalid NRIC is submitted', async () => {
// Arrange
const schema = ValidationOptional.args?.schema
render(<ValidationOptional />)
const input = screen.getByLabelText(schema!.title) as HTMLInputElement
const submitButton = screen.getByText('Submit')

expect(input.value).toBe('')

// Act
userEvent.type(input, 'S0000001B')
userEvent.click(submitButton)
await waitFor(() => submitButton.textContent !== 'Submitting')

// Assert
// Should show error message.
const error = screen.getByText('Please enter a valid NRIC')
expect(error).not.toBeNull()
})
})
38 changes: 38 additions & 0 deletions frontend/src/templates/Field/Nric/NricField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @precondition Must have a parent `react-hook-form#FormProvider` component.
*/
import { useMemo } from 'react'
import { useFormContext } from 'react-hook-form'

import { FormFieldWithId, NricFieldBase } from '~shared/types/field'

import { createNricValidationRules } from '~utils/fieldValidation'
import Input from '~components/Input'

import { BaseFieldProps, FieldContainer } from '../FieldContainer'

export type NricFieldSchema = FormFieldWithId<NricFieldBase>
export interface NricFieldProps extends BaseFieldProps {
schema: NricFieldSchema
}

export const NricField = ({
schema,
questionNumber,
}: NricFieldProps): JSX.Element => {
const validationRules = useMemo(
() => createNricValidationRules(schema),
[schema],
)

const { register } = useFormContext()

return (
<FieldContainer schema={schema} questionNumber={questionNumber}>
<Input
aria-label={schema.title}
{...register(schema._id, validationRules)}
/>
</FieldContainer>
)
}
14 changes: 14 additions & 0 deletions frontend/src/utils/fieldValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import simplur from 'simplur'

import {
FieldBase,
NricFieldBase,
ShortTextFieldBase,
TextSelectedValidation,
UenFieldBase,
} from '~shared/types/field'
import { isNricValid } from '~shared/utils/nric-validation'
import { isUenValid } from '~shared/utils/uen-validation'

import { REQUIRED_ERROR } from '~constants/validation'
Expand Down Expand Up @@ -69,3 +71,15 @@ export const createUenValidationRules = (
},
}
}

export const createNricValidationRules = (
schema: NricFieldBase,
): RegisterOptions => {
return {
...createBaseValidationRules(schema),
validate: (val?: string) => {
if (!val) return true
return isNricValid(val) || 'Please enter a valid NRIC'
},
}
}

0 comments on commit 6a0fbb1

Please sign in to comment.