-
react-hook-form typescript로 변환 및 컴포넌트 생성 (antd)Language/React 2023. 12. 27. 17:42
이번 포스팅에서는 react-hook-form을 전체 사이트에 적용하기에 앞서 코드 라인을 줄여서 가독성을 높이고 타입스크립트로 컴포넌트를 제작해 두려고 한다.
현재 javascript 및 typescript가 혼재 되어 있지만.. 시간될때 마다 수정해 나가자! 언젠간.. typescript만 남으리라!!
react-hook-form component 적용
import { useForm } from 'react-hook-form'; import { NInput, NInputNumber, NTextArea, NCheckbox, NSelect, NRadioGroup, NTinyMceEditor, NImageSelector } from 'components/FormControls'; const TestPop = (props) => { ... const { handleSubmit, watch, formState: { errors }, control, reset, } = useForm({ defaultValues: {}, }); ... return ( ... <NInput name="TITLE" control={control} rules={{ required: '제목은 필수 입력입니다.' }} placeholder="제목을 입력해주세요." maxLength={50} /> <NInputNumber name="WEIGHT" control={control} placeholder="정렬 가중치를 입력해주세요." maxLength={4} /> <NCheckbox label="고방" name="GOBANG_CATEGORY_YN" control={control} checked={watch('GOBANG_CATEGORY_YN')} /> <NRadioGroup name="DISPLAY_TYPE" control={control} rules={{ required: '공개설정은 필수 입력입니다.' }}> {Object.keys(DISPLAY_TYPES).map((item, idx) => ( <Radio key={idx} value={item}> {DISPLAY_TYPES[item]} </Radio> ))} </NRadioGroup> <NSelect name="TAG_ITEMS" control={control} rules={{ required: '태그는 필수 입력입니다.' }} mode="tags" tokenSeparators={[' ']} placeholder="태그 선택" options={options} /> <NTextArea name="DESCRIPTION" control={control} placeholder="그룹 요약을 입력해주세요." rows={2} showCount={true} maxLength={200} /> <NTinyMceEditor name="CONTENT" control={control} rules={{ required: '콘텐츠 내용은 필수 입력입니다.' }} // placeholder="aa" height="50vh" /> <NImageSelector name="TITLE_IMAGE" control={control} rules={{ required: '커버이미지는 필수 입력입니다.' }} type="single" /> <NImageSelector control={control} name="IMAGE1" value={watch('IMAGE1')} rules={{ required: true }} /> {props.target === 'THEME' && ( <NImageSelector control={control} name="IMAGE2" value={watch('IMAGE2')} rules={{ required: true }} /> )} <NDatePicker control={control} name="RECEIPT_FROM_DT" rules={{ required: '접수시작일은(는) 필수 입력입니다.' }} placeholder="접수시작일 선택" allowClear={true} disabledDate={(current) => current && current < moment().subtract(1, 'days').endOf('day')} onChange={(date, dateString) => { const theFromYmd = date ? date.format('YYYYMMDD') : null; const theToYmd = watch('RECEIPT_TO_DT') ? watch('RECEIPT_TO_DT').format('YYYYMMDD') : null; if (theFromYmd !== null && theToYmd !== null && theFromYmd > theToYmd) { // RECEIPT_FROM_DT.isAfter(RECEIPT_TO_DT) setValue('RECEIPT_TO_DT', null); } }} /> ... ); }
FormControls.tsx
import React from 'react'; import { Input, InputNumber, Checkbox, Radio, Select, DatePicker } from 'antd'; import TinyMceEditor from 'components/TinyMceEditor'; import ImageSingle from 'components/ImageSingle'; // import ImageSelector from 'components/ImageSelector'; // import CorverImageSelector from 'components/CorverImageSelector'; import moment from 'moment'; import { Controller } from 'react-hook-form'; import FormErrorMessage from 'components/FormErrorMessage'; interface NProps { // type: string; // label: string; control: any; name: string; rules: Record<string, any>; placeholder: string; children: React.ReactNode; disabled?: boolean; showErrorMessage?: boolean; } interface NInputProps extends NProps { maxLength: number; showCount?: boolean; } export const NInput = ({ placeholder = '', maxLength, showCount = false, ...rest }: NInputProps) => { return ( <Controller name={rest.name} control={rest.control} rules={rest.rules} render={({ field, fieldState }) => ( <> {/* type={type} bordered={false} */} <Input {...field} maxLength={maxLength} showCount={showCount} placeholder={placeholder} disabled={rest.disabled} className={fieldState.invalid ? 'error' : ''} /> {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && ( <FormErrorMessage message={fieldState.error?.message} /> )} </> )} /> ); }; interface NInputNumberProps extends NProps { maxLength: number; } export const NInputNumber = ({ placeholder = '', maxLength, ...rest }: NInputNumberProps) => { return ( <Controller name={rest.name} control={rest.control} rules={rest.rules} render={({ field, fieldState }) => ( <> <InputNumber {...field} maxLength={maxLength} placeholder={placeholder} disabled={rest.disabled} className={fieldState.invalid ? 'error' : ''} /> {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && ( <FormErrorMessage message={fieldState.error?.message} /> )} </> )} /> ); }; interface NCheckboxProps extends NProps { label: string; checked?: boolean; } export const NCheckbox = ({ label, checked, ...rest }: NCheckboxProps) => { return ( <Controller name={rest.name} control={rest.control} rules={rest.rules} render={({ field, fieldState }) => ( <> <Checkbox {...field} disabled={rest.disabled} className={fieldState.invalid ? 'error' : ''} checked={checked}> {label} </Checkbox> {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && ( <FormErrorMessage message={fieldState.error?.message} /> )} </> )} /> ); }; interface NRadioGroupProps extends NProps {} export const NRadioGroup = ({ ...rest }: NRadioGroupProps) => { return ( <Controller name={rest.name} control={rest.control} rules={rest.rules} render={({ field, fieldState }) => ( <> <div className={'row-box' + (fieldState.invalid ? ' error' : '')}> <Radio.Group {...field} disabled={rest.disabled} className={fieldState.invalid ? 'error' : ''}> {rest.children} </Radio.Group> </div> {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && ( <FormErrorMessage message={fieldState.error?.message} /> )} </> )} /> ); }; interface NSelectOption { label: string | number | null; value: string | number | null; } interface NSelectProps extends NProps { mode?: 'multiple' | 'tags'; options?: NSelectOption[]; tokenSeparators?: string[]; } export const NSelect = ({ mode, placeholder = '', options, tokenSeparators, ...rest }: NSelectProps) => { return ( <Controller name={rest.name} control={rest.control} rules={rest.rules} render={({ field, fieldState }) => ( <> <Select {...field} disabled={rest.disabled} className={fieldState.invalid ? 'error' : ''} placeholder={placeholder} mode={mode} tokenSeparators={tokenSeparators} options={options} > {rest.children} </Select> {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && ( <FormErrorMessage message={fieldState.error?.message} /> )} </> )} /> ); }; interface NTextAreaProps extends NProps { rows: number; maxLength: number; showCount?: boolean; } export const NTextArea = ({ placeholder = '', rows, maxLength, showCount = false, ...rest }: NTextAreaProps) => { return ( <Controller name={rest.name} control={rest.control} rules={rest.rules} render={({ field, fieldState }) => ( <> <Input.TextArea {...field} placeholder={placeholder} rows={rows} showCount={showCount} maxLength={maxLength} disabled={rest.disabled} className={fieldState.invalid ? 'error' : ''} /> {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && ( <FormErrorMessage message={fieldState.error?.message} /> )} </> )} /> ); }; interface NTinyMceEditorProps extends NProps { height?: string; } export const NTinyMceEditor = ({ placeholder = '', height = '30vh', ...rest }: NTinyMceEditorProps) => { return ( <Controller name={rest.name} control={rest.control} rules={rest.rules} render={({ field, fieldState }) => ( <> <div className={'row-box' + (fieldState.invalid ? ' error' : '')}> <TinyMceEditor {...field} height={height} content={field.value} placeholder={placeholder} onChange={(content: string) => { field.onChange(content); }} /> </div> {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && ( <FormErrorMessage message={fieldState.error?.message} /> )} </> )} /> ); }; interface NImageSelectorProps extends NProps { // type?: string | undefined; } export const NImageSelector = ({ ...rest }: NImageSelectorProps) => { return ( <Controller name={rest.name} control={rest.control} rules={rest.rules} render={({ field, fieldState }) => ( <> <div className={'row-image-box' + (fieldState.invalid ? ' error' : '')}> <ImageSingle {...field} label="사진선택" // file={field.value && { src: field.value }} file={field.value} onChange={(value: { src: any } | null) => { if (value !== null && !Array.isArray(value)) { field.onChange(value); } else { field.onChange(null); } }} /> {/* <CorverImageSelector src={field.value && field.value} onChange={(file: { src: any } | null) => { if (file !== null) { field.onChange(file.src); } else { field.onChange(null); } }} /> */} </div> {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && ( <FormErrorMessage message={fieldState.error?.message} /> )} </> )} /> ); }; type DatePickerOnChange = (date: moment.Moment | null, dateString: string) => void; interface NDatePickerProps extends NProps { disabledDate: (current: any | null) => any; allowClear?: boolean; value?: moment.Moment | null | undefined; onChange?: DatePickerOnChange; } export const NDatePicker: React.FC<NDatePickerProps> = ({ placeholder = '', ...rest }) => { return ( <Controller name={rest.name} control={rest.control} rules={rest.rules} render={({ field, fieldState }) => ( <> <DatePicker {...field} placeholder={placeholder} className={fieldState.invalid ? 'error' : ''} value={rest.value !== undefined ? rest.value : field.value && moment(field.value, 'YYYYMMDD')} // value={field.value && moment(field.value, 'YYYYMMDD')} disabled={rest.disabled} disabledDate={rest.disabledDate} allowClear={rest.allowClear} onChange={(date, dateString) => { field.onChange(date); // Trigger the React Hook Form onChange event if (rest.onChange) { rest.onChange(date, dateString); // Call the passed onChange prop } }} /> {rest.showErrorMessage !== false && fieldState.invalid && fieldState.error?.message && ( <FormErrorMessage message={fieldState.error?.message} /> )} </> )} /> ); };
ImageSingle.tsx
import React, { useContext } from 'react'; import { Image } from 'antd'; import { Request } from 'lib/common'; import { BaseContext } from 'components/Contexts'; interface ImageSingleProps { label?: string | undefined; file?: { src: any } | null | undefined; className?: string | undefined; thumbWith?: number | undefined; thumbHeight?: number | undefined; onChange: (file: { src: any } | null) => void; } const ImageSingle: React.FC<ImageSingleProps> = ({ label = '사진선택', file, className = '', ...props }) => { const context = useContext(BaseContext); const handleImageChange = async (e: { target: { files: any } }) => { const formData = new FormData(); // formData.append('THUMB_WIDTH', props.thumbWith ? props.thumbWith : 104 * 2); // formData.append('THUMB_HEIGHT', props.thumbHeight ? props.thumbHeight : 104 * 2); formData.append('THUMB_WIDTH', props.thumbWith ? String(props.thumbWith) : String(104 * 2)); formData.append('THUMB_HEIGHT', props.thumbHeight ? String(props.thumbHeight) : String(104 * 2)); for (const file of e.target.files) { formData.append('file', file); } const response = await Request.upload('/image', formData); if (response.status === 200) { props.onChange({ src: response.locationList[0] }); } else { context.alert({ title: ( <> 한 번에 업로드 가능한 이미지 용량은 최대 20MB를 초과할 수 없습니다. <br /> 다수의 이미지를 등록 하신다면, 하나씩 나눠서 업로드하는 것을 권장합니다. </> ), buttons: [{ text: '확인', type: 'primary', danger: true }], }); } }; const handleDelete = (idx: number) => { props.onChange(null); }; return ( <div className={'wrap-image-selector ' + className}> {file ? ( <div className="wrap-images"> <ImageItem idx={0} file={file} onDelete={handleDelete} /> </div> ) : ( <div className="select-image"> {label} <input type="file" accept="image/png, image/gif, image/jpeg, image/bmp, image/x-icon, image/webp" // multiple={type !== 'single'} onChange={handleImageChange} /> </div> )} </div> ); }; interface ImageItemProps { idx: number; file?: { src: any } | null | undefined; onDelete: (arg0: any) => void; } const ImageItem: React.FC<ImageItemProps> = ({ file, ...props }) => { const context = useContext(BaseContext); return ( <div className="item"> <div> <Image src={file && file.src} alt="" className="center-cropped" /> <button className="btn-delete" onClick={(e) => { e.stopPropagation(); context.alert({ title: '삭제 하시겠습니까?', buttons: [ { text: '삭제', type: 'primary', danger: true, onClick: () => { props.onDelete(props.idx); }, }, { text: '취소' }, ], }); }} > <span>삭제</span> </button> </div> </div> ); }; export default ImageSingle;
버전도 너무 옛날 버전이고 부분부분 바꿀려니 개노가다네 ㅋ
어거지로 할려니 개빡시당..ㅠ 아자아자!!
rules - pattern
<NInput control={control} name="TITLE" ... rules={{ required: '제목은(는) 필수 입력입니다.' }} rules={{ required: '제목은(는) 필수 입력입니다.', pattern: /^\S+@\S+$/i }} rules={{ required: { value: true, message: '제목은(는) 필수 입력입니다.' }, pattern: /^\S+@\S+$/i }} rules={{ required: { value: true, message: '제목은(는) 필수 입력입니다.' }, pattern: { value: /^\S+@\S+$/i, message: '정규식이 맞지 않아...' }, }} rules={{ pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i, message: '이메일 형식이 아닙니다.', }, minLength: { value: 8, message: '비밀번호는 8자 이상이여야 합니다,', }, }} ... placeholder="제목을(를) 입력해주세요." maxLength={300} />
rules - validate
<NInputNumber control={control} name="BIRTH_YEAR" rules={{ required: { value: true, message: '출생연도를 입력해주세요.' }, validate: (value: string) => { const currentYear = new Date().getFullYear(); const birthYear = parseInt(value); const age = currentYear - birthYear - 1; // 만나이 if (age < 0 || age > 100) { return '올바른 출생년도를 입력하세요.'; } else if (age < 14) { return '만 14세 이상만 가입이 가능합니다.'; } }, }} placeholder="출생연도 입력" maxLength={4} />
rules - pattern & validate
<NInput control={control} name="NICKNAME" rules={{ required: { value: true, message: '닉네임을 입력해주세요.' }, pattern: { value: /^[가-힣a-zA-Z0-9]*$/, message: '띄어쓰기 없이 한글, 영문, 숫자만 가능합니다.', // value: /^[가-힣a-zA-Z0-9]{2,12}$/, // message: '2~12내에서 띄어쓰기 없이 한글, 영문, 숫자만 가능합니다.', }, validate: { lengthCheck: (value: string) => (value.length >= 2 && value.length <= 12) || '2에서 12자 사이로 입력하세요.', }, }} placeholder="닉네임 입력" maxLength={12} />
상황에 따라 필수여부 변경
rules={{ required: watch('floatingButtonShowYn') === 'Y' ? '플로팅 랜딩 URL은 필수 입력입니다.' : false }}
'Language > React' 카테고리의 다른 글
nextJS 사용하는 iOS 단말기에서 뒤로가기 페이지 흰색 이슈 (0) 2024.06.20 nextjs에서 svg 컴포넌트로 사용하는 방법 (0) 2024.04.04 react에 typescript 적용하기 (1) 2023.12.27 react-hook-form 적용기 (antd) (0) 2023.12.18 NextJS에서 queryString 변경 감지 hook 만들기 (0) 2023.10.20