Add new post

add new code sandbox component
This commit is contained in:
DefectingCat
2022-08-12 11:01:18 +08:00
parent b9bc6cdef6
commit 047fc0c775
16 changed files with 520 additions and 75 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,14 +1,11 @@
import classNames from 'classnames';
import useMounted from 'lib/hooks/useMounted';
import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react';
import { FiMoon, FiSun } from 'react-icons/fi';
const DarkModeBtn = () => {
const [mounted, setMounted] = useState(false);
const { mounted } = useMounted();
const { systemTheme, theme, setTheme } = useTheme();
// When mounted on client, now we can show the UI
useEffect(() => setMounted(true), []);
const currentTheme = theme === 'system' ? systemTheme : theme;
if (!mounted)

View File

@ -0,0 +1,72 @@
import classNames from 'classnames';
import useInView from 'lib/hooks/useInView';
import { useTheme } from 'next-themes';
import { useCallback, useEffect, useRef, useState } from 'react';
import RUALoading from './loading/RUALoading';
const partten =
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/;
const commonClass = classNames(
'rounded-lg h-[500px] border-0',
'overflow-hidden w-full'
);
type Props = {
url: string;
};
const RUACodeSandbox = ({ url }: Props) => {
const isUrl = partten.test(url);
const { systemTheme, theme } = useTheme();
const currentTheme = theme === 'system' ? systemTheme : theme ?? 'light';
const { ref, inView } = useInView();
const sandUrl = new URL(url);
const embed = sandUrl.pathname.split('/')[2];
const [src, setSrc] = useState('');
useEffect(() => {
inView &&
setSrc(
`https://codesandbox.io/embed/${embed}?fontsize=14&hidenavigation=1&theme=${currentTheme}&view=preview`
);
}, [currentTheme, embed, inView]);
const [load, setLoad] = useState(false);
const handleLoad = useCallback(() => {
setLoad(true);
}, []);
if (!isUrl) return null;
return (
<>
<div className={classNames(commonClass, 'relative')}>
<div
className={classNames(
commonClass,
'absolute flex items-center justify-center',
load && 'hidden',
'transition-all z-10'
)}
>
<RUALoading />
</div>
<iframe
ref={ref}
src={src}
className={classNames(
commonClass,
!load && 'blur-sm',
'transition-all'
)}
allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
onLoad={handleLoad}
></iframe>
</div>
</>
);
};
export default RUACodeSandbox;

View File

@ -1,3 +0,0 @@
.head:hover:before {
content: unset !important;
}

View File

@ -1,64 +0,0 @@
import { getHeadings } from 'lib/utils';
import Anchor from 'components/mdx/Anchor';
import styles from './PostTOC.module.css';
import classNames from 'classnames';
import { useCallback, useState } from 'react';
import { FiChevronDown } from 'react-icons/fi';
interface Props {
headings: ReturnType<typeof getHeadings>;
}
const PostTOC = ({ headings }: Props) => {
const [show, setShow] = useState(false);
const handleClick = useCallback(() => setShow((show) => !show), []);
return (
<>
<div
className={classNames(
'rounded-lg transition-all',
'duration-500 overflow-hidden',
'my-4'
)}
style={{
maxHeight: show ? (headings?.length ?? 0) * 50 + 70 : 70,
}}
>
<h2
className={classNames(
styles.head,
'bg-white !m-[unset] p-4',
'rounded-lg border border-gray-300',
'dark:bg-rua-gray-800 dark:border-rua-gray-600',
'select-none cursor-pointer',
'flex justify-between items-center',
'!text-2xl'
)}
onClick={handleClick}
>
<span>What&apos;s inside?</span>
<FiChevronDown
className={classNames(
show && 'rotate-180',
'transition-all duration-500'
)}
/>
</h2>
<ul className="pl-4 border-l-4 border-gray-300 toc">
{headings?.map((h) => (
<li key={h.link}>
<Anchor href={h.link} external={false}>
{h.text}
</Anchor>
</li>
))}
</ul>
</div>
</>
);
};
export default PostTOC;

View File

@ -1,9 +1,9 @@
import { useEffect, useState } from 'react';
import useMounted from 'lib/hooks/useMounted';
import { useEffect } from 'react';
import tocbot from 'tocbot';
const SlideToc = () => {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const { mounted } = useMounted();
useEffect(() => {
// Waiting the right time.

12
lib/hooks/useMounted.tsx Normal file
View File

@ -0,0 +1,12 @@
import { useState, useEffect } from 'react';
const useMounted = () => {
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
return {
mounted,
};
};
export default useMounted;

View File

@ -0,0 +1,44 @@
const app = `import { useForm } from 'react-hook-form';
type Pet = 'Cat' | 'Dog';
type FormData = {
firstName: string;
lastName: string;
favorite: Pet;
};
export default function App() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>();
const onSubmit = handleSubmit((data) => console.log(data));
return (
<div>
<form onSubmit={onSubmit}>
<div>
<label htmlFor="firstname">First name:</label>
<input type="text" id="firstname" {...register('firstName')} />
</div>
<div>
<label htmlFor="lastname">Last name:</label>
<input type="text" id="lastname" {...register('lastName')} />
</div>
<div>
<label htmlFor="favorite">Favorite pet:</label>
<select id="favorite" {...register('favorite')}>
<option value="cat">Cat</option>
<option value="dog">Dog</option>
</select>
</div>
<button>Submit</button>
</form>
</div>
);
}`;
export default app;

View File

@ -0,0 +1,338 @@
---
title: 组件泛型实例-封装可复用的表单组件
date: '2022-08-12'
tags: [TypeScript, React]
---
export const meta = {
title: '组件泛型实例-封装可复用的表单组件',
date: '2022-08-12',
tags: ['TypeScript', 'React'],
};
import Layout from 'layouts/MDXLayout';
import dynamic from 'next/dynamic';
import Image from 'components/mdx/Image';
import image0 from 'assets/images/p/generic-component-encapsulate-reusable-component/Untitled.png';
import image1 from 'assets/images/p/generic-component-encapsulate-reusable-component/Untitled.png';
import image2 from 'assets/images/p/generic-component-encapsulate-reusable-component/Untitled.png';
import image3 from 'assets/images/p/generic-component-encapsulate-reusable-component/Untitled.png';
import image4 from 'assets/images/p/generic-component-encapsulate-reusable-component/Untitled.png';
import image5 from 'assets/images/p/generic-component-encapsulate-reusable-component/Untitled.png';
import app from './hook-form-basic/App.ts';
import app2 from './react-generic/App.ts';
import child from './react-generic/Child.ts';
export const RUASandpack = dynamic(() => import('components/RUA/RUASandpack'));
export const RUACodeSandbox = dynamic(() =>
import('components/RUA/RUACodeSandbox')
);
export default ({ children }) => <Layout {...meta}>{children}</Layout>;
当前很多 UI 库都会为我们集成可复用的 Form 组件,并且是开箱即用。但有时候我们往往可能需要为自己的组件集成 Form。单纯的手动管理所有的状态可能不是件理想的活尤其是表单验证。
[React Hook Forms](https://react-hook-form.com/) 为我们提供了完善的状态管理,并且可以集成到任何组件中去。
你可能会问,如今已经有了像是 MUI、Ant Design 等此类优秀的组件库,为什么还需要使用 React Hook Forms 来管理表单。
[MUI: The React component library you always wanted](https://mui.com/zh/)
虽然一些优秀的成熟组件库会为我们提供良好的表单解决方案,但它终究需要与组件库一起使用。而并非只是提供表单的状态管理,并没有完全的与组件库解耦合。
同时,当我们使用诸如 [Daisyui](https://daisyui.com/) 等此类的 CSS 组件时,它们是与状态完全解耦合的。我们需要自己为其维护状态。
## Hook our form
对于一个表单来说,提供的表单项越多,所需要的状态管理就越繁琐。不仅仅是状态管理,后续的表单验证才是一个表单的核心所在。
React Hook Forms 对 TypeScript 支持良好,有了 TypeScript 我们就可以在开发时验证表单类型。而表单的数据类型也是后续封装通用组件较为繁琐的一个地方。
<RUASandpack
template="react-ts"
files={{
'/App.tsx': app,
}}
customSetup={{
dependencies: {
'@emotion/react': '^11.10.0',
'@emotion/styled': '^11.10.0',
'react-hook-form': '^7.34.0',
},
}}
/>
React Hook Forms 在使用方面,使用了一个 `register` 函数代替了我们为每个表单项管理状态的步骤。从写法上就可以看出,这个函数返回了我们的表单所需要的属性,以及其状态。
```tsx
<input type="text" id="firstname" {...register('firstName')} />
```
在表单提交方面,`handleSubmit` 方法接受一个回调,其参数就是表单输入后的状态。
```tsx
const onSubmit = handleSubmit((data) => console.log(data));
```
表单验证通过后,就可以成功调用这个函数,以实现我们的表单提交。
这是一段最基础的用法,没有表单验证提示,仅仅只是接受任何用户输入的数据。并且同样的组件也没有实现复用。
## Input 组件
封装一个可复用的 `Input` 组件可能是再简单不过的事情了,对于其参数类型,主要部分还是来自于 `HTMLInput` 。我们只需要个别定义的属性,再利用剩余参数将其全部赋值给真正的 `input`
```tsx
export type FormInputProps = {
label?: string | undefined;
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
```
```tsx
const Input = ({ name, label, ...rest }: FormInputProps) => {
return (
<>
<label htmlFor={name}>{label}</label>
<S.Wrapper>
<S.Input name={name} {...rest} />
</S.Wrapper>
</>
);
};
```
用起来自然也是和常见的组件一样方便:
```tsx
<div>
<Input name="firstname" label="First name:" />
</div>
<div>
<Input name="lastname" label="Last name:" />
</div>
```
但是如果仅仅只是这样,我们的组件还不能与 React Hook Forms 一起工作。因为其核心部分 `register` 函数还无法传递给我们的 `Input` 组件。也就是说我们的组件现在还是不可控的,这时候再尝试提交就会发现无法获取其状态。
<Image src={image0} alt="无法获取其状态" />
当然我们不能简单的将 `register` 函数塞给 `Input` 组件,因为它还没有合适的签名。`register` 函数会根据表单的数据签名和不同的表单项来实现自己的签名。
从 `register` 函数的签名中就可以看出,它接受一个泛型,该泛型就是对应的表单项类型。
```tsx
register: <"firstName">(name: "firstName", options?: ...)
```
也就是 `FormData` 中的 `firstName`
```tsx
type FormData = {
firstName: string;
lastName: string;
favorite: Pet;
};
```
没错,要想正确的给组件中的 `register` 函数签名,我们就得给我们的函数式组件上个泛型。
## 泛型
在考虑给组件添加一个泛型之前,需要先简单的了解下泛型是如何工作的。
一个函数的泛型可以非常的简单,它代表了一个任意的类型值(当然也可以对其进行约束)。并根据指定的参数为泛型时,自动推断该类型值。
```ts
const logAndReturn = <T extends unknown>(target: T) => {
console.log(target);
return target;
};
// const logAndReturn: <42>(target: 42) => 42
logAndReturn(42);
// const logAndReturn: <"42">(target: "42") => "42"
logAndReturn('42');
```
### 类型别名中的泛型
类型别名中的泛型与函数不同的是,它需要手动传递一个函数的泛型值(或来自其他地方的泛型),并根据该泛型来决定其值。并且如果泛型有约束的话,还需要符合其约束。
例如,我们有一个描述个人的类型别名:
```ts
type Person<T extends number | string> = {
name: string;
age: number;
favorite: T;
};
```
而我们需要编写一个函数,根据其 `favorite` 来决定打印的值。函数大概长这样:
```tsx
const sayIt = <T extends number | string>(p: Person<T>) => {
const type = typeof p.favorite;
switch (type) {
case 'string':
console.log(`My favorite word is: ${p.favorite}`);
return;
case 'number':
console.log(`My favorite number is: ${p.favorite}`);
return;
}
};
```
当指定参数为 `p: Person<T>` 时,就需要将函数的泛型传递给类型别名。且类型别名中的泛型约束在了 `<T extends number | string>` 之间,函数必须保证使其子类型。否则就会提示无法满足其类型。
<Image src={image1} alt="未约束的泛型" />
和参数类型,泛型也是向下兼容的,只要保证其类型是子类型即可。也就是说这样也是可以的 `const sayIt = <T extends 42>(p: Person<T>) => {}` 。数字 42 是 `number` 类型的子类型。
随后在调用函数时,就能发现泛型给我们带来的作用了。
<Image src={image2} alt="传递数字给泛型" />
<Image src={image3} alt="传递字符串给泛型" />
### React 中的泛型
我们的 React 函数组件也是一个函数,对于泛型的规则同样适用。
来看一个简单的小组件,该组件可以以一个常见的对象类型 `Record<string, unknown>` 来根据指定的 key 访问其值,并展示在 DOM 上。
<RUASandpack
template="react-ts"
files={{
'/App.tsx': app2,
'/Child.tsx': child,
}}
/>
例如,这样的一个值:
```tsx
const testData = {
firstName: 'xfy',
lastName: 'xfyxfy',
};
```
当传递其对应的 key 时,我们的子组件就会展示对应的属性。也就是 `data[key]` ,这是再简单不过的一个属性访问方式了。
```tsx
<Child data={testData} name="firstName" />
```
但不仅如此,我们还希望我们的子组件能够根据已经存在的值,推断出我们能够传递的 key。
<Image src={image4} alt="类型推断" />
这正是泛型的作用。
首先,我们子组件的参数签名必然需要一个泛型。并且我们将泛型约束在为一个常见的对象 `Record<string, unknown>`且不在乎属性值具体是什么类型unknown
```tsx
type Props<T extends Record<string, unknown>> = {
name: keyof T;
data: T;
};
```
这便是我们组件的参数具体的签名。还记得上述类型别名需要将函数的泛型传递给它吗?接下来就是要给函数式组件添加一个泛型,并将其传递给 `Props` 。
我们的组件也是一个标准的函数,所以接下来就简单多了。只需要将泛型正确的约束,并传递给别名即可。
```tsx
const Child = <T extends Record<string, unknown>>({
name,
data,
}: Props<T>) => {};
```
```tsx
const Child = <T extends Record<string, unknown>>({ name, data }: Props<T>) => {
const [showName, setShowName] = useState<T[keyof T]>();
const valid = () => {
console.log(data[name]);
setShowName(data[name]);
};
return (
<>
<div>{name}</div>
<button onClick={valid}>Show {name}</button>
<div>{JSON.stringify(showName)}</div>
</>
);
};
```
## 带有泛型的 Input 组件
`register` 函数对表单项的验证与上述较为类似,它也会根据表单项的 key 来决定传递对应的 name。为了满足 `register` 函数,可复用的 Input 组件就得需要一个泛型,用来接受不同的表单数据类型。
React hook forms 为我们提前准备好了适用于 `register` 函数的类型别名 `UseFormRegister` ,它会接受一个泛型,该泛型就是我们的表单数据类型。
所以 `register` 函数的签名看起来就像这样 `register?: UseFormRegister<T>;` 这里的 `T` 就是我们的表单类型。但是我们还不知道传入当前组件中的表单类型是什么,所以我们的组件参数签名也需要一个泛型。
所以这里我们的组件参数看起来是这样的:
```tsx
export type FormInputProps<TFormValues> = {
name: Path<TFormValues>;
label?: string | undefined;
rules?: RegisterOptions;
register?: UseFormRegister<TFormValues>;
} & DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>;
```
值得注意的是,这里给 `input` 使用的 name 属性。因为 `register` 函数注册时使用的名称需要确保为表单类型的中的一个。所以这里需要使用 React hook forms 导出的 `Path<TFormValues>` 类型,以配合 `register` 函数。
这里就和上述泛型组件很相似了,接下来要做的就是将组件的泛型传递给参数签名:
```tsx
const Input = <T extends Record<string, unknown>>({
name,
label,
...rest
}: FormInputProps<T>) => {};
```
这里给组件的泛型小小的约束一下,我们希望传递过来的表单类型是一个普通的对象结构 `<T extends Record<string, unknown>>` 。
不仅如此,还不能忘了 `register` 函数还需要注册在 DOM 上。
```tsx
<S.Input
err={!!errorMsg}
name={name}
{...(register && register(name, rules))}
{...rest}
/>
```
得益于泛型的功劳,我们将 `register` 函数传递给 `Input` 组件时,我们的组件就知道了这次表单的类型。并且确定了 `name` 属性的类型。
<Image src={image5} alt="类型推断" />
这是因为 `register` 函数本身的签名:`const register: UseFormRegister<FormData>` 。这才使得我们的组件成功接受到了泛型。
再添加一些 `rules` 以及验证未通过时的提示,这样一个可复用的 React hook form 组件就封装好了。
```tsx
<Input
name="lastName"
label="Last name:"
placeholder="Last Name"
register={register}
rules={{ required: true }}
errorMsg={errors.lastName && 'Please input'}
/>
```
<RUACodeSandbox url="https://codesandbox.io/s/reusable-input-o7e4jt?file=/src/App.tsx" />

View File

@ -0,0 +1,22 @@
const app = `import "./styles.css";
import Child from "./Child";
const testData = {
name: "xfy",
age: 18
};
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div>
<Child data={testData} name="name" />
</div>
</div>
);
}`;
export default app;

View File

@ -0,0 +1,27 @@
const child = `import { useState } from "react";
type Props<T extends Record<string, unknown>> = {
name: keyof T;
data: T;
};
const Child = <T extends Record<string, unknown>>({ name, data }: Props<T>) => {
const [showName, setShowName] = useState<T[keyof T]>();
const valid = () => {
console.log(data[name]);
setShowName(data[name]);
};
return (
<>
<div>{name}</div>
<button onClick={valid}>Show {name}</button>
<div>{JSON.stringify(showName)}</div>
</>
);
};
export default Child;`;
export default child;