您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~
前言
React-View-UI已经更新了25个组件了,今天来写一下Message组件,这个组件目前还是很常用的,组件效果如下:
组件库文档如下:
具体的交互可以在react-view-ui.com:92/#/common/me…进行体验。
API能力
一共提供了如下的API:
组件主要调用方式如下:
直接调用:
Message.info('This is an info message!')
传入对象调用(扩展API都需要传入对象):
Message.info({
content: 'This is an info message!',
duration: 3000,
position: 'bottom'
clearable: true,
style: {
fontSize: '12px'
}
});
组件类型接口
import { CSSProperties } from 'react';
interface MessageProps<T> {
/**
* @description 对象类型传参时的内容
*/
content?: T;
/**
* @description Message类型
*/
type?: 'info' | 'success' | 'warning' | 'error' | 'normal' | 'loading';
/**
* @description 显示时间
* @default 3000ms
*/
duration?: number;
/**
* @description 显示位置
* @default top
*/
position?: 'top' | 'bottom';
/**
* @description 出现可清除按钮
* @default false
*/
clearable?: boolean;
/**
* @description 自定义样式
* @default {}
*/
style?: CSSProperties;
}
export type { MessageProps };
组件源码
使用者所调用的Message.xxx函数其实就是在调用组件内部所定义的addInstance函数,往页面中不断加入div(Message组件),并在一定时延后自动关闭,这是组件的整体设计思路。 通过add和remove两个方法对消息窗进行添加和删除,并在删除后进行整体布局(高度)重排,即可实现自动合并的效果。
import React, { useState, useEffect, useMemo, useRef, CSSProperties } from 'react';
import ReactDOM from 'react-dom';
import { MessageProps } from './interface';
import './index.module.less';
import {
ExclamationCircleFilled,
CheckCircleFilled,
CloseCircleFilled,
LoadingOutlined,
CloseOutlined,
} from '@ant-design/icons';
let container: HTMLDivElement | null;
let topMessageNum: number = 0;
let bottomMessageNum: number = 0;
function addInstance(
type: 'info' | 'success' | 'warning' | 'error' | 'normal' | 'loading',
props: string | MessageProps<string>,
) {
let style: CSSProperties = {},
duration: number = 3000,
content,
position: 'top' | 'bottom' = 'top',
clearable = false;
if (typeof props === 'object') {
style = props.style || {};
duration = props.duration || 3000;
content = props.content;
position = props.position ? props.position : 'top';
clearable = props.clearable ? props.clearable : false;
} else if (typeof props === 'string') {
content = props;
}
const div = document.createElement('div');
const messageBoxId = String(Math.floor(Math.random() * 1000));
div.setAttribute('class', `${position}-${messageBoxId}`);
if (container) {
container.appendChild(div);
} else {
container = document.createElement('div');
container.setAttribute('class', 'all-container');
document.body.appendChild(container);
container.appendChild(div);
}
setTimeout(() => {
if (Array.prototype.slice.call(container?.childNodes).includes(div)) {
changeHeight(Array.prototype.slice.call(container?.childNodes), position);
container?.removeChild(div);
if (position === 'top') {
topMessageNum--;
} else {
bottomMessageNum--;
}
}
}, duration + 200);
ReactDOM.render(
<Message
style={style}
content={content}
type={type}
duration={duration}
position={position}
clearable={clearable}
messageBoxId={messageBoxId}
/>,
div,
);
}
function remove(id: string, position: string) {
//重排节点下元素高度
const container = document.querySelector('.all-container');
const children = Array.prototype.slice.call(container?.childNodes);
for (let key in children) {
if (children[key].getAttribute('class') === `${position}-${id}`) {
const removeDom = children[key];
container?.removeChild(removeDom);
if (position === 'top') {
topMessageNum--;
} else {
bottomMessageNum--;
}
changeHeight(children.slice(Number(key)), position);
}
}
}
function changeHeight(children: Array<HTMLElement>, position: any) {
for (let key in children) {
const child = children[key].childNodes[0] as HTMLElement;
if (children[key].getAttribute('class')?.startsWith(position)) {
child.style[position] = Number(child.style[position].split('p')[0]) - 70 + 'px';
}
}
}
const Message = (props: MessageProps<string>) => {
const { style, content, type, duration, position, clearable, messageBoxId } = props;
const [opac, setOpac] = useState(1);
const messageDom = useRef<any>(null);
useEffect(() => {
if (position === 'top') {
topMessageNum++;
} else {
bottomMessageNum++;
}
setTimeout(() => {
(messageDom.current as HTMLElement).style.transition = '0.2s linear';
(messageDom.current as HTMLElement).style.animation = 'none';
}, 500);
setTimeout(() => {
setOpac(0);
}, duration);
}, []);
useEffect(() => {
const transform = position || 'top';
(messageDom?.current as HTMLElement).style[transform] =
(transform === 'top' ? topMessageNum : bottomMessageNum) * 70 + 'px';
}, [topMessageNum, bottomMessageNum]);
const messageIcon = useMemo(() => {
if (type === 'info') {
return <ExclamationCircleFilled style={{ color: '#1890ff', fontSize: '16px' }} />;
} else if (type === 'error') {
return <CloseCircleFilled style={{ color: '#f53f3f', fontSize: '16px' }} />;
} else if (type === 'normal') {
return <></>;
} else if (type === 'success') {
return <CheckCircleFilled style={{ color: '#19b42a', fontSize: '16px' }} />;
} else if (type === 'warning') {
return <ExclamationCircleFilled style={{ color: '#fa7d00', fontSize: '16px' }} />;
} else if (type === 'loading') {
return <LoadingOutlined style={{ color: '#1890ff', fontSize: '16px' }} />;
}
}, [type]);
const closeMessage = () => {
remove(messageBoxId as string, position as string);
};
return (
<div className="message-container" style={{ opacity: opac, ...style }} ref={messageDom}>
{messageIcon}
<span className="toast-content">{content}</span>
{clearable && <CloseOutlined onClick={closeMessage} />}
</div>
);
};
Message.info = (props: string | MessageProps<string>) => {
return addInstance('info', props);
};
Message.success = (props: string | MessageProps<string>) => {
return addInstance('success', props);
};
Message.error = (props: string | MessageProps<string>) => {
return addInstance('error', props);
};
Message.normal = (props: string | MessageProps<string>) => {
return addInstance('normal', props);
};
Message.warning = (props: string | MessageProps<string>) => {
return addInstance('warning', props);
};
Message.loading = (props: string | MessageProps<string>) => {
return addInstance('loading', props);
};
export default Message;
组件测试
Message组件的单元测试代码如下:
import React from 'react';
import Message from '../../Message/index';
import Enzyme from '../setup';
import mountTest from '../mountTest';
const { mount } = Enzyme;
describe('Message', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.runAllTimers();
});
it('test base Message show correctly', () => {
Message.info('this is a test');
expect(document.querySelectorAll('.all-container')).toHaveLength(1);
expect(document.querySelectorAll('.message-container .toast-content')[0].innerHTML).toBe(
'this is a test',
);
});
it('test click five nums Message show num correctly', () => {
for (let i = 0; i < 5; i++) {
Message.info('content');
}
expect(document.querySelectorAll('.all-container')[0].childNodes.length).toBe(5);
});
it('test bottom transform Message show correctly', () => {
Message.info({
content: 'this is a test',
duration: 3000,
position: 'bottom',
});
expect(
document.querySelectorAll('.message-container')[0].getAttribute('style')?.includes('bottom:'),
);
});
it('test clearable Message correctly', () => {
Message.info({
content: 'this is a test',
duration: 3000,
clearable: true,
});
expect(document.querySelectorAll('.message-container')[0].childNodes.length).toBe(3);
});
});
组件库地址
- Concis组件库线上链接:react-view-ui.com:92
- github:github.com/fengxinhhh/…
- npm:www.npmjs.com/package/con…
开源不易,欢迎学习和体验,喜欢请多多支持,有问题请留言。