Code Monkey home page Code Monkey logo

form's Introduction

Form

Documentation

Intro

Basic commands:

  • yarn start = start the test project

Technologies

  • react, mobx, typescript

Goal

  • for fun form library which handle the form/form field validations, set the onChangeHandler and value changes automatically.
  • make the form handling more comfortable and easier with ReactJs.

Benefits

Advantages

  • work with any of 3 mode or you can mix it up
  • automatical set the model and validate it based on validations (if validation fail the onsubmit not triggered)
  • work with form validation (which happens only at click to the onSubmit) or field validation which happens on the input change
  • work with custom input components (example at hybrid mode CustomUsernameInput)
  • type safe
  • handle multiple input types (input with datalist), select, file imput with single or file array etc
    • use Input, Select, Custom (for special components) components and Form

Modes

Class & Decorator approach - click here for expand
Decorator List
// rules
@required // input field must have a value
@isNumber // value is a valid number or not
@minLength(number) // string length validation
@maxLength(number) // string length validation
@min(number) // lesser than validation
@max(number) // greater than validation
@isChecked(errorMessage?: string) // give error if the checkbox not checked
@rule(key: string) // example rules was declared in RegExp.ts and used like a regExp validation for the value
@minSize(number) // min file size
@maxSize(number) // max file size
@minTotalSize(number) // min files size (in case if multiple file selected, input must have multiple={true} props)
@maxTotalSize(number) // max files size (in case if multiple file selected, input must have multiple={true} props)
@minCount(number) // array length lesser than
@maxCount(number) // array length higher than
@allowedType(string) // example 'image', 'jpg', 'jpeg', 'png' etc
@isSame(string or keyof T or getterFunctionWhichReturnAValue) // compare value with other property/value or with constant

// props
@label(string) // input label
@fileCmp(ReactComponent) // replace the original file input component
@accept(string) // input file accept example 'image/*'
@multiple  // allow file select input to select multiple files
@valueParser((value: any) => any) // convert the value from 1 type into another
@props(object) // prop object for inputs
@options([string, string][]) // options for datalist, select and radio; example [['m', 'Male'], ['f', 'Female']]
@cmp(ReactComponent) // use custom component which reiceive the '{ fieldStore }' like props
Class & Decorators
@Form({ autoGenerate: true, showErrors: true })
class RegisterDto {

    @minLength(3)
    @rule('ALPHA_NUM')
    @label('Username: ')
    public username: string = '';

    @minLength(6)
    @rule('NAME')
    @label('Fullname: ')
    public fullname: string = '';

    @required
    @type('email')
    @label('Email: ')
    public email: string = '';

    @type('password')
    @rule('LOW_UP_NUM')
    @minLength(8)
    @label('Password: ')
    public password: string = '';

    @type('checkbox')
    @label('I Agree')
    @isChecked('Please check in the agreement') // it is same than @validator(agree => !agree && ['Please check in the agreement'])
    public agree: boolean = false;

    @type('radio') // it is same than: @props({ type: 'radio', options: [{ label: 'Male', value: 'm' }, { label: 'Female', value: 'f' }] })
    @options(() => [['m', 'Male'], ['f', 'Female']])
    public sex: string;

    @type('file')
    @allowedType('image')
    @maxSize(2 * 1024 * 1024)
    @fileCmp(CustomFileUploader)
    public image: File;
}
Component
const RegisterForm = () => {
    const registerDto = useConstant(() => new RegisterDto());
    const onSubmit = React.useCallback(async (data: RegisterDto) => {
        // dummy waiting time, return true if you sent successfully and false if not
        await delayPromise(1000);
        console.info('result', data);
        alert(JSON.stringify(data));
        return true;
    }, [])

    return (
        <div className='register-form1'>
            <h1> "With Decorators" form </h1>
            <Form<RegisterDto>
                entity={registerDto}
                onSubmit={onSubmit}
            />
        </div>
    );
}
Config object approach - click here for expand
validatorMap
type IErrorParams = (string | number)[]; // if we need some dynamic value for error
type IErrorText = string; // simple error message
type IErrorMsg = [IErrorText, IErrorParams?];
  
const validatorMap = {
    minSize: <V>(size: number, errorMsg: string = 'TOO_SMALL_FILE') => (value: V) => false | IErrorMsg,
    maxSize: <V>(size: number, errorMsg: string = 'TOO_BIG_FILE') => (value: V) => false | IErrorMsg,
    minTotalSize: <V>(size: number, errorMsg: string = 'TOO_SMALL_FILE') => (value: V) => false | IErrorMsg,
    maxTotalSize: <V>(size: number, errorMsg: string = 'TOO_BIG_FILE') => (value: V) => false | IErrorMsg,
    minCount: <V>(count: number, errorMsg: string = 'TOO_LESS') => (value: V) => false | IErrorMsg,
    maxCount: <V>(count: number, errorMsg: string = 'TOO_MUCH') => (value: V) => false | IErrorMsg,
    allowedType: <V>(typeOrExtension: string, errorMsg: string = 'INVALID_FILE') => (value: V) => false | IErrorMsg,
    minLength: <V>(minLength: number, errorMsg: string = 'TOO_SHORT') => (value: V) => false | IErrorMsg,
    maxLength: <V>(maxLength: number, errorMsg: string = 'TOO_LONG') => (value: V) => false | IErrorMsg,
    min: <V>(min: number, errorMsg: string = 'TOO_LOW') => (value: V) => false | IErrorMsg,
    max: <V>(max: number, errorMsg: string = 'TOO_HIGH') => (value: V) => false | IErrorMsg,
    isNumber: (errorMsg: string = 'NOT_A_NUMBER') => (value: IValue) => false | IErrorMsg,
    rule: <V>(rule: keyof IRules, errorMsg: string = 'INVALID_FORMAT') => (value: V) => false | IErrorMsg,
    isChecked: (errorMsg: string = 'MUST_BE_CHECKED') => (value: boolean) => false | IErrorMsg,
    required: (errorMsg: string = 'IS_REQUIRED') => (value: unknown) => false | IErrorMsg,
    isSame: <T extends object>(valueOrGetter: IValueOrGetter<T>, errorMsg: string = 'NOT_SAME') => (value: IValue, obj: T) => false | IErrorMsg,
};
config
const config = {
    autoGenerate: true,
    showErrors: true,
    fields: {
        username: {
            rule: 'ALPHA_NUM',
            label: 'Username: '
        },
        fullname: {
            minLength: 6,
            rule: 'NAME',
            label: 'Fullname: '
        },
        email: {
            required: true,
            type: 'email',
            label: 'Email: '
        },
        password: {
            rule: 'LOW_UP_NUM',
            minLength: 8,
            type: 'password',
            label: 'Password: ',
        },
        agree: {
            type: 'checkbox',
            label: 'I Agree',
            validators: [
                validatorMap.isChecked('Please check in the agreement')
            ],
            value: false
        },
        sex: {
            type: 'radio',
            options: [
                { label: 'Male', value: 'm' },
                { label: 'Female', value: 'f' }
            ]
        },
        image: {
            type: 'file',
            FileCmp: CustomFileUploader,
            validators: [
                validatorMap.allowedType('image'),
                validatorMap.maxSize(2 * 1024 * 1024)
            ],
        }
    }
} as IConfig<RegisterDto>;
Component
const RegisterForm = () => {

    const onSubmit = React.useCallback(async (data: RegisterDto) => {
        // dummy waiting time
        await delayPromise(1000);
        console.info('result', data);
        alert(JSON.stringify(data));
        return true;
    }, [])

    return (
        <div className='register-form2'>
            <h1> "With Config" form </h1>
            <Form<RegisterDto>
                config={config}
                onSubmit={onSubmit}
            />
        </div>
    );
}  
Props approach - click here for expand
props
  
export type IFormProps<T extends object> = Omit<React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLFormElement>, HTMLFormElement>, 'onSubmit'> & {
    autoDisable?: boolean; // enable or disable the inputs with type 'submit' and 'reset'
    autoGenerate?: boolean; // if it is enabled then auto generate the input children for the form
    store?: FormStore<T>; // if we want provide an external formStore
    entity?: T;
    config?: IConfig<T>;
    showErrors?: boolean; // show form errors

    debounceTime?: number; // if this is setted then submit method called with debounce

    errorRender?: (errors: string[], values?: T) => JSX.Element; // custom form error renderer
    onSubmit: (data: T) => Promise<boolean | void>; // submit method
    validator?: (values: T) => IErrorMsg; // for validator, can prevent the submitting if it return an error message
}


export type ITranslateFn = (str: string, params?: (string | number)[], options?: Record<string, any>) => string;
export type ICustomFormFieldProps<V, T extends object, H = HTMLInputElement> = { fieldStore: IFieldStore<V, T, H> };

export interface ICommonInputProps<V, T extends object, H = HTMLInputElement> {
    name: keyof T;

    value?: IValue;
    defaultValue?: IValue;
    disabled?: boolean;
    label?: string | JSX.Element;
    valueParser?: (value: IValue) => IValue;

    labelStyle?: React.CSSProperties;
    fieldStyle?: React.CSSProperties;
    fieldClassName?: string;
    labelClassName?: string;
    showErrors?: boolean;

    entity?: T;
    validators?: IValidator<V, T>[];
    translateFn?: ITranslateFn;
    Cmp?: ({ fieldStore }: ICustomFormFieldProps<V, T, H>) => JSX.Element | null;
    errorRender?: (errors: string[]) => JSX.Element; // custom form error renderer
}

export type IInputProps<V, T extends object> = React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLInputElement>, HTMLInputElement> & ICommonInputProps<V, T> & {
    type?: 'number' | 'text' | 'reset' | 'submit' | 'checkbox' | 'radio' | 'password' | 'email' |
    'date' | 'datetime-local' | 'time' | 'week' | 'month' | 'color' | 'file' | 'range';

    accept?: string;
    multiple?: boolean;
    checked?: boolean;
    FileCmp?: (props: { onClick: () => void, value?: V, fieldStore: IFieldStore<V, T> }) => JSX.Element;

    rule?: keyof IRules;
    required?: boolean;
    pattern?: string;
    minLength?: number;
    maxLength?: number;
    min?: number | string;
    max?: number | string;

    step?: number;
    list?: string;
    options?: { label: string, value: string }[]; /* for radio */
}

export type ISelectProps<V, T extends object> = React.DetailedHTMLProps<React.FormHTMLAttributes<HTMLSelectElement>, HTMLSelectElement> & ICommonInputProps<V, T> & {
    type?: 'number' | 'text';
    options: { label: string, value: string }[];
}

export type ICustomProps<V, T extends object> = Partial<IInputProps<V, T> & ISelectProps<V, T>> & { Cmp: Pick<ICommonInputProps<V, T>, 'Cmp'>, name: keyof T };

export type IFieldProps<P, T extends object> = IInputProps<P, T> | ISelectProps<P, T> | ICustomProps<P, T>;

export interface IFormFieldOptions<T extends object> {
    errorStore: ErrorStore<T>;
    props: IFormProps<T>;
    setValue?: (value: IValue) => void;
    getValues: () => T;
    isDirty: () => boolean;
}
Component
const RegisterForm = () => {

    const onSubmit = React.useCallback(async (data: RegisterDto) => {
        // dummy waiting time
        await delayPromise(1000);
        console.info('result', data);
        alert(JSON.stringify(data));
        return true;
    }, [])

    return (
        <div className='register-form3'>
            <h1> "With Props" form </h1>
            <Form<RegisterDto>
                showErrors
                onSubmit={onSubmit}
            >
                <Input name='username' rule='ALPHA_NUM' label='Username: ' />
                <Input name='fullname' rule='NAME' label='Fullname: ' minLength={6} />
                <Input name='email' type='email' label='Email: ' required />
                <Input name='password' type='password' rule='LOW_UP_NUM' label='Password: ' minLength={8} />
                <Input name='agree' type='checkbox' label='I Agree: ' validators={[validatorMap.isChecked('Please check in the agreement')]} value={false} />
                <Input name='sex' type='radio' options={[{ label: 'Male', value: 'm' }, { label: 'Female', value: 'f' }]} />
                <Input name='image' type='file' FileCmp={CustomFileUploader} validators={[validatorMap.allowedType('image'), validatorMap.maxSize(2 * 1024 * 1024)]} />

                <Input name='reset' value='Reset' type='reset' />
                <Input name='submit' value='Submit' type='submit' />
            </Form>
        </div>
    );
};
Hybrid approach + Custom Input Cmp - click here for expand
Decorator
// the form decorator is optional but we can provide here the constant configs (props)
class RegisterDto {

    public id = Guid.newGuid(); // normal field

    @minLength(3)
    @rule('ALPHA_NUM')
    @label('Username: ')
    public username: string = '';

    @minLength(6)
    @rule('NAME')
    @label('Fullname: ')
    public fullname: string = '';

    @required
    @type('email')
    @label('Email: ')
    public email: string = '';

    @type('password')
    @rule('LOW_UP_NUM')
    @minLength(8)
    @label('Password: ')
    public password: string = '';
  
    @type('password')
    @isSame('password')
    @label('Confirm: ')
    public password2: string = '';


    public agree: boolean = false;
    public sex: string = 'f'; // example a default value
    public image: File;
}
Config
const config = {
    fields: {
        agree: {
            validators: [
                validatorMap.isChecked('Please check in the agreement')
            ],
        },
        sex: {
            options: [
                { label: 'Male', value: 'm' },
                { label: 'Female', value: 'f' }
            ]
        },
        image: {
            validators: [
                validatorMap.allowedType('image'),
                validatorMap.maxSize(2 * 1024 * 1024)
            ],
        }
    }
} as IConfig<RegisterDto>;
CustomUsernameInput
const CustomUsernameInput: ICustomProps<string, RegisterDto>['Cmp'] = observer(({ fieldStore }) => {
    return (
        <div
            className={cn('form-field', fieldStore.errors.length > 0 && 'error')}
            data-type='text'
            title={fieldStore.translatedErrors.join('\r\n') || ''}
        >
            <label htmlFor='username' className='form-label'> Username: </label>
            <input
                id='username'
                {...fieldStore.getProps(['showErrors']) as any}
                value={fieldStore.value}
            />
        </div>
    );
});  
Form Component
const RegisterForm = observer(() => {

    const onSubmit = React.useCallback(async (data: RegisterDto) => {
        // dummy waiting time
        await delayPromise(1000);
        console.info('result', data);
        alert(JSON.stringify(data));
        return true;
    }, [])

    const formStore = useConstant(() => new FormStore({ onSubmit }));
    const registerDto = useConstant(() => new RegisterDto());

    return (
        <div className='register-form4'>
            <h1> "With Hybrid Mode" form </h1>
            <Form<RegisterDto>
                showErrors
                entity={registerDto}
                config={config}
                store={formStore}
                onSubmit={onSubmit}
            >
                <Custom<string, RegisterDto> Cmp={CustomUsernameInput} name='username' />
                <Input name='fullname' />
                <Input name='email' />
                <Input name='password' />
                <Input name='agree' type='checkbox' label='I Agree: ' value={false} />
                <Input name='sex' type='radio' />
                <Input name='image' type='file' FileCmp={CustomFileUploader} />

                <Input name='reset' value='Reset' type='reset' />
                <Input name='submit' value='Submit' type='submit' />
            </Form>
        </div>
    );
});

Get started

What is important?

  • You must use component only the package, like Form, Input, Select, Custom
  • if you use custom components then probably you must wrap your component with observer from react-mobx-lite package

In use

// the form itself
const RegisterForm = () => {

    const onSubmit = React.useCallback(async (data: RegisterDto) => {
        // dummy waiting time
        await delayPromise(1000);
        console.info('result', data);
        alert(JSON.stringify(data));
        return true;
    }, [])

    return (
        <div className='register-form3'>
            <h1> "With Props" form </h1>
            <Form<RegisterDto>
                showErrors
                onSubmit={onSubmit}
            >
                <Input name='username' rule='ALPHA_NUM' label='Username: ' />
                <Input name='fullname' rule='NAME' label='Fullname: ' minLength={6} />
                <Input name='email' type='email' label='Email: ' required />
                <Input name='password' type='password' rule='LOW_UP_NUM' label='Password: ' minLength={8} />
                <Input<string, RegisterDto> name='password2' />
                <Input name='agree' type='checkbox' label='I Agree: ' validators={[validatorMap.isChecked('Please check in the agreement')]} value={false} />
                <Input name='sex' type='radio' options={[{ label: 'Male', value: 'm' }, { label: 'Female', value: 'f' }]} />
                <Input name='image' type='file' FileCmp={CustomFileUploader} validators={[validatorMap.allowedType('image'), validatorMap.maxSize(2 * 1024 * 1024)]} />

                <Input name='reset' value='Reset' type='reset' />
                <Input name='submit' value='Submit' type='submit' />
            </Form>
        </div>
    );
};
  
// file uploader preview component - this just for fun
const CustomFileUploader: IInputProps<File, any>['FileCmp'] = ({ onClick, value }) => {

    const [src, setSrc] = React.useState<string>('');

    React.useEffect(() => {
        if (!value) {
            setSrc('');
        } else {
            file2Base64(value).then((source) => setSrc(source))
        }
    }, [value, setSrc])

    return (
        <img
            src={src}
            onClick={onClick}
            width={100}
            height={100}
            style={{ background: 'white', border: '1px solid #777', margin: 'auto', cursor: 'pointer' }}
        />
    );
};

If you want to be sure you used the right property name at name field then you can use generic types (we have generics in other 2 approach as well)

  <Input<boolean, RegisterDto> type='checkbox' name='agree' />

form's People

Contributors

shadowvzs avatar

Stargazers

 avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.