Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add component: FormHelper #12

Merged
merged 1 commit into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/FormHelper/autoFocus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React, { MutableRefObject, useState } from 'react';

type RefOrDomOrId<T> = HTMLElement | string | React.Ref<T>;

type F = any;

const getDom = (refOrDomOrId: RefOrDomOrId<F>) =>
typeof refOrDomOrId === 'string'
? document.querySelector(`#${refOrDomOrId}`)
: (refOrDomOrId as MutableRefObject<F>)?.current || refOrDomOrId;

// https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/input#input_%E7%B1%BB%E5%9E%8B
const disabled = ':not([disabled]):not([readonly])';
const queryInputTypes =
['text', 'password', 'search', 'tel', 'url', 'number', 'email', '']
.map(item => `input[type="${item}"]${disabled}`)
.join(', ') + `, input:not([type])${disabled}, textarea${disabled}`;
const setAutoFocus = (refOrDomOrId: RefOrDomOrId<F>) => {
const dom = getDom(refOrDomOrId);
if (!dom) return;
const input = dom.querySelector?.(queryInputTypes);
if (!input?.focus) return;
input.focus();
return true;
};

/**
* Passing a ref, id, or DOM element to obtain and set the focus state of the first non-disabled and non-readonly input or textarea.
* @param {RefOrDomOrId} refOrDomOrId - ζ”―ζŒη±»εž‹ HTMLElement | string | React.Ref<T>
* @returns void
*/
export const useAutoFocus = (refOrDomOrId?: RefOrDomOrId<F>) => {
const [focused, setFocused] = useState<boolean>(false);
React.useEffect(() => {
if (focused || !refOrDomOrId) return;
const setRes = setAutoFocus(refOrDomOrId);
if (!setRes) return;
setFocused(true);
}, [refOrDomOrId, focused, setFocused]);
};
26 changes: 26 additions & 0 deletions src/FormHelper/demos/DomUseAutoFocus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useAutoFocus } from '@yuntijs/ui';
import React, { useEffect, useState } from 'react';

import FormContent from './components/FormContent';
import RenderContainer from './components/RenderContainer';

type IDomUseAutoFocus = Record<string, never>;

const FormContentWrapper = () => {
const [fomDom, setFormDom] = useState<HTMLElement | null>(null);
useEffect(() => {
// <Form id="form123"> in FormContent
setFormDom(document.querySelector('#form123') as HTMLElement);
}, []);
useAutoFocus(fomDom);
return <FormContent />;
};

const DomUseAutoFocus: React.FC<IDomUseAutoFocus> = () => {
return (
<RenderContainer>
<FormContentWrapper />
</RenderContainer>
);
};
export default DomUseAutoFocus;
22 changes: 22 additions & 0 deletions src/FormHelper/demos/FormHelperDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FormHelper } from '@yuntijs/ui';
import { Button, Form, Input } from 'antd';
import React, { useState } from 'react';

const Test = () => {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={setOpen.bind('', !open)}>Click me!</Button>
{open && (
<FormHelper>
<Form>
<Form.Item>
<Input />
</Form.Item>
</Form>
</FormHelper>
)}
</>
);
};
export default Test;
15 changes: 15 additions & 0 deletions src/FormHelper/demos/HocFormHelper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { withFormHelper } from '@yuntijs/ui';
import React from 'react';

import FormContent from './components/FormContent';
import RenderContainer from './components/RenderContainer';

const HocFormHelper: React.FC<Record<string, never>> = () => {
const FormContentWrapper = withFormHelper()(FormContent);
return (
<RenderContainer>
<FormContentWrapper />
</RenderContainer>
);
};
export default HocFormHelper;
20 changes: 20 additions & 0 deletions src/FormHelper/demos/IdUseAutoFocus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useAutoFocus } from '@yuntijs/ui';
import React from 'react';

import FormContent from './components/FormContent';
import RenderContainer from './components/RenderContainer';

const FormContentWrapper = () => {
// <Form id="form123"> in FormContent
useAutoFocus('form123');
return <FormContent />;
};

const IdUseAutoFocus: React.FC<Record<string, never>> = () => {
return (
<RenderContainer>
<FormContentWrapper />
</RenderContainer>
);
};
export default IdUseAutoFocus;
25 changes: 25 additions & 0 deletions src/FormHelper/demos/RefUseAutoFocus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useAutoFocus } from '@yuntijs/ui';
import React, { useRef } from 'react';

import FormContent from './components/FormContent';
import RenderContainer from './components/RenderContainer';

const FormContentWrapper = () => {
const divRef = useRef(null);
useAutoFocus(divRef);
// ⚠️warning The form instance obtained through the ref of the Ant Design's Form component does not take effect. You need to pass the ref of native HTML tags like div or span.
return (
<div ref={divRef}>
<FormContent />
</div>
);
};

const RefUseAutoFocus: React.FC<Record<string, never>> = () => {
return (
<RenderContainer>
<FormContentWrapper />
</RenderContainer>
);
};
export default RefUseAutoFocus;
34 changes: 34 additions & 0 deletions src/FormHelper/demos/components/FormContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Form, Input, InputNumber, Radio, Select, Switch } from 'antd';
import React from 'react';

const FormContent: React.FC<Record<string, never>> = () => {
return (
<Form id="form123" initialValues={{ select: '' }}>
<div>
<input id="originRadio" type="radio" value="111" />
<label htmlFor="originRadio">HTML native radio</label>
</div>
<Form.Item label="select" name={'select'}>
<Select disabled placeholder="hello select" showSearch>
<Select.Option>123</Select.Option>
</Select>
</Form.Item>
<Form.Item label="switch" name="inputNumber">
<InputNumber />
</Form.Item>
<Form.Item label="Radio">
<Radio>123</Radio>
</Form.Item>
<Form.Item label="switch" name="enabled">
<Switch />
</Form.Item>
<Form.Item label="password">
<Input.Password placeholder="hello" />
</Form.Item>
<Form.Item label="username">
<Input placeholder="hello" />
</Form.Item>
</Form>
);
};
export default FormContent;
19 changes: 19 additions & 0 deletions src/FormHelper/demos/components/RenderContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Button } from 'antd';
import React, { useState } from 'react';

interface IRenderContainer {
children: React.ReactNode;
}

const RenderContainer: React.FC<IRenderContainer> = props => {
const [open, setOpen] = useState(false);
return (
<>
<Button onClick={setOpen.bind('', !open)}>Click me!</Button>
<br />
<br />
{open && props.children}
</>
);
};
export default RenderContainer;
53 changes: 53 additions & 0 deletions src/FormHelper/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
nav: Components
group: Utils
title: FormHelper
description: Enhanced component for Form
---

# FormHelper

Form enhancement component, currently supporting:

- autoFocus: automatically selecting the first non-disabled and non-readonly input or textarea of the form when focused.

## Usage

### React component FormHelper

Wrap the Form component with FormHelper. Supported attributes include:

| props | description | default | required |
| --------- | -------------------------------------------------------------------------------------------------------------- | ------- | -------- |
| autoFocus | Automatically select the focus state of the first non-disabled and non-readonly input or textarea in the form. | true | false |
| id | The id of the outer wrapping div. | - | false |
| className | The className of the outer wrapping div. | - | false |
| style | The style of the outer wrapping div. | - | false |

#### usage

<code src="./demos/FormHelperDemo"></code>

### withFormHelper - Higher-Order Component (HOC)

As a higher-order component, withFormHelper supports the same parameters as the React component FormHelper.

#### usage

<code src="./demos/HocFormHelper"></code>

### useAutoFocus - hooks

Supports passing a ref, id, or DOM element as a parameter.

#### Example of a ref parameter:

<code src="./demos/RefUseAutoFocus"></code>

#### Example of a id parameter:

<code src="./demos/IdUseAutoFocus"></code>

#### Example of a dom parameter:

<code src="./demos/DomUseAutoFocus"></code>
50 changes: 50 additions & 0 deletions src/FormHelper/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import React, { ComponentType, FC, useRef } from 'react';

import { useAutoFocus } from './autoFocus';

export { useAutoFocus } from './autoFocus';
// Component
export type FormHelperProps = {
children: React.ReactNode;
autoFocus?: boolean;
id?: string;
className?: string;
style?: React.CSSProperties;
};

const FormHelper: React.FC<FormHelperProps> = props => {
const { autoFocus = true } = props;

const ref = useRef(null);
useAutoFocus(autoFocus ? ref : undefined);
return (
<div
className={props.className}
id={props.id}
ref={ref}
style={props.style ?? { display: props.className ? undefined : 'contents' }}
>
{props.children}
</div>
);
};

// HOC
type FormHelperConfig = Omit<FormHelperProps, 'children'>;

export const withFormHelper =
(formHelperConfig?: FormHelperConfig) =>
<P extends Record<string, never>>(WrappedComponent: ComponentType<P>): FC<P> => {
const HocComponent: FC<P> = props => {
return (
<FormHelper {...(formHelperConfig || {})}>
<WrappedComponent {...props} />
</FormHelper>
);
};
const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
HocComponent.displayName = `withFormHelper(${displayName})`;
return HocComponent;
};

export { FormHelper };
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export * from './Card';
export * from './Descriptions';
export * from './Divider';
export * from './Drawer';
export * from './FormHelper';
export * from './Modal';

// ~ antd
Expand Down Expand Up @@ -64,7 +65,7 @@ export {
type FormRule,
Grid,
Image,
ImageProps,
type ImageProps,
Input,
InputNumber,
type InputNumberProps,
Expand Down
Loading