预览
Button | Vue3xy (chengbotao.github.io)
Button | Reactxy (chengbotao.github.io)
介绍
常用的操作按钮&文本链接
支持设置按钮尺寸
支持设置按钮类型
Props 属性
支持原生
Button和A属性
| 属性名 | 属性类型 | 说明 | 默认值 |
|---|---|---|---|
disabled | boolean | 设置 Button 禁用 | false |
btnType | oneOf "primary" | "danger" | "link" | "default" | 设置 Button 类型 | default |
href | string | 当 Button 为 link 类型时的地址 | - |
className | string | 自定义CSS类名 | - |
size | oneOf "sm" | "lg" | 设置 Button 尺寸 | - |
Event 事件
| 事件名 | 参数类型 | 说明 |
|---|---|---|
click | MouseEvent | 点击事件 |
呈现
Vue3 实现
<template>
<button v-if="!isLink" key="button" v-bind="$attrs" :disabled="disabled" :class="classes" @click="handleClick">
<slot></slot>
</button>
<a v-else key="a" v-bind="$attrs" :href="href" :class="classes" @click="handleClick">
<slot></slot>
</a>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'XyButton',
inheritAttrs: false,
})
</script>
<script setup lang="ts">
import { computed } from "vue";
import classNames from "classnames";
type ButtonSize = "lg" | "sm";
type ButtonType = "primary" | "default" | "danger" | "link";
interface ButtonProps {
disabled?: boolean
href?: string
className?: string
size?: ButtonSize
btnType?: ButtonType
}
interface ButtonEmits {
(event: "click", payload: MouseEvent): void
}
// defineOptions({
// name: 'XyButton',
// inheritAttrs: false,
// })
// Props
const props = withDefaults(defineProps<ButtonProps>(), {
btnType: "default"
});
// Emits
const emits = defineEmits<ButtonEmits>();
// computed
const classes = computed(() => {
return classNames("xy-button", props.className, {
[`xy-button-${props.btnType}`]: props.btnType,
[`xy-button-${props.size}`]: props.size,
disabled: props.btnType === "link" && props.disabled,
});
});
const isLink = computed(() => {
return props.btnType === "link" && !!props.href;
})
// methods
const handleClick = (event: MouseEvent) => {
emits("click", event)
}
// public 方法
defineExpose({})
</script>
vitest & @testing-library/vue 单元测试
import { render } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import Button from './index';
describe('Button Component', () => {
// 应呈现正确的默认按钮
test('should render the correct default button', async () => {
const { getByText, emitted } = render(Button, {
slots: {
default: 'Default Button',
},
});
const element = getByText('Default Button') as HTMLButtonElement;
expect(element.tagName).toEqual('BUTTON');
expect(element.disabled).toBeFalsy();
expect(element.className).toMatch(/xy-button xy-button-default/);
await userEvent.click(element);
expect(emitted('click')).toBeTruthy();
});
// 应该根据不同的props渲染正确的组件
test('should render the correct component based on different props', () => {
const { getByText } = render(Button, {
slots: {
default: 'Primary Button',
},
props: {
btnType: 'primary',
size: 'lg',
className: 'xy-custom-btn',
},
});
const element = getByText('Primary Button') as HTMLButtonElement;
expect(element.className).toMatch(/xy-button xy-custom-btn xy-button-primary xy-button-lg/);
});
// 当 btnType 等于 link 并提供 href 时,应该呈现一个 A 标签
test('should render a link when btnType equals link and href is provided', () => {
const { getByText } = render(Button, {
slots: {
default: 'Link Button',
},
props: {
btnType: 'link',
href: '#',
},
});
const element = getByText('Link Button') as HTMLAnchorElement;
expect(element.tagName).toEqual('A');
expect(element.className).toMatch(/xy-button xy-button-link/);
});
// 应呈现禁用按钮当 disabled 设置为 true
test('should render disabled button when disabled set to true', async () => {
const { getByText, emitted } = render(Button, {
slots: {
default: 'Disabled Button',
},
props: {
disabled: true,
},
});
const element = getByText('Disabled Button') as HTMLButtonElement;
expect(element.disabled).toBeTruthy();
await userEvent.click(element);
expect(emitted('click')).toBeFalsy();
});
});
React 实现
import React, { FC, AnchorHTMLAttributes, ButtonHTMLAttributes } from 'react';
import classNames from 'classnames';
export type ButtonSize = 'lg' | 'sm';
export type ButtonType = 'primary' | 'default' | 'danger' | 'link';
export interface ButtonProps
extends Partial<ButtonHTMLAttributes<HTMLElement> & AnchorHTMLAttributes<HTMLElement>> {
/** 设置 Button 禁用 */
disabled?: boolean;
/** 设置 Button 尺寸 */
size?: ButtonSize;
/** 设置 Button 类型 */
btnType?: ButtonType;
/** 当 Button 为 link 类型时的地址 */
href?: string;
/** 自定义 css 类名 */
className?: string;
children?: React.ReactNode;
}
/**
* Button 组件
*
*
*/
export const Button: FC<ButtonProps> = (props) => {
const { btnType, disabled, className, size, href, children, ...restProps } = props;
const classes = classNames('xy-button', className, {
[`xy-button-${btnType}`]: btnType,
[`xy-button-${size}`]: size,
disabled: btnType === 'link' && disabled,
});
if (btnType === 'link' && href) {
return (
<a className={classes} href={href} {...restProps}>
{children}
</a>
);
} else {
return (
<button className={classes} disabled={disabled} {...restProps}>
{children}
</button>
);
}
};
Button.defaultProps = {
disabled: false,
btnType: 'default',
};
export default Button;
jest & @testing-library/react 单元测试
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import Button, { ButtonProps } from './button';
describe('Button Component', () => {
// 应呈现正确的默认按钮
test('should render the correct default button', () => {
const defaultProps = {
onClick: jest.fn(),
};
const wrapper = render(<Button {...defaultProps}>Default Button</Button>);
const element = wrapper.getByText('Default Button') as HTMLButtonElement;
expect(element).toBeInTheDocument();
expect(element.tagName).toEqual('BUTTON');
expect(element.disabled).toBeFalsy();
expect(element).toHaveClass('xy-button xy-button-default');
fireEvent.click(element);
expect(defaultProps.onClick).toHaveBeenCalled();
});
// 应该根据不同的props渲染正确的组件
test('should render the correct component based on different props', () => {
const differentProps: ButtonProps = {
btnType: 'primary',
size: 'lg',
className: 'xy-custom-btn',
};
const wrapper = render(<Button {...differentProps}>Primary Button</Button>);
const element = wrapper.getByText('Primary Button') as HTMLButtonElement;
expect(element).toBeInTheDocument();
expect(element).toHaveClass('xy-button xy-button-primary xy-custom-btn');
});
// 当 btnType 等于 link 并提供 href 时,应该呈现一个 A 标签
test('should render a link when btnType equals link and href is provided', () => {
const linkandhrefProps: ButtonProps = {
btnType: 'link',
href: '#',
};
const wrapper = render(<Button {...linkandhrefProps}>Link Button</Button>);
const element = wrapper.getByText('Link Button') as HTMLAnchorElement;
expect(element).toBeInTheDocument();
expect(element.tagName).toEqual('A');
expect(element).toHaveClass('xy-button xy-button-link');
});
// 应呈现禁用按钮当 disabled 设置为 true
test('should render disabled button when disabled set to true', () => {
const disabledProps: ButtonProps = {
disabled: true,
onClick: jest.fn(),
};
const wrapper = render(<Button {...disabledProps}>Disabled Button</Button>);
const element = wrapper.getByText('Disabled Button') as HTMLButtonElement;
expect(element).toBeInTheDocument();
expect(element.disabled).toBeTruthy();
fireEvent.click(element);
expect(disabledProps.onClick).not.toHaveBeenCalled();
});
});
请教
有以下问题向大佬们请教,请不吝赐教
-
如何开发
Vue3组件的类型声明?了解了
elementUI的实现方式,但在实现中不顺畅搁浅了,后续会再研究 -
vue3使用defineOptions来声明name等属性时,导出的组件打印出来实例还是没有声明属性?
后记
Vue-Button源码
React-Button源码
Introduction | Testing Library (testing-library.com)
user-event | Testing Library (testing-library.com)
个人博客 | Botaoxy (chengbotao.github.io)
感谢阅读,敬请斧正!