React15.6.0实现Modal弹层组件

代码地址以下:<br>http://www.demodashi.com/demo/12315.htmlcss

注:本文Demo环境使用的是我平时开发用的配置:这里是地址html

本文适合对象

  1. 了解React。
  2. 使用过webpack3。
  3. 熟悉es6语法。

项目说明

项目结构截图

项目运行说明

  1. npm install
  2. npm run start
  3. npm run startfe
  4. 登陆localhost:8088查看demo

Modal组件分析

Modal组件是属于一个网站中比较经常使用的基础组件,可是在实现方面上稍微复杂一些,对场景支持的需求度较高。node

这里是Antd中Modal组件的演示Demoreact

首先分析这个组件的组成结构:webpack

  1. title Modal弹层的标题部分。
  2. content Modal弹层的主体部分。
  3. footer Modal弹层最后的button部分。
  4. background 整个黑色背景

其次,这个弹层不能生硬的出现,因此必定要有动画效果。css3

最后,弹层是在合适的地方经过用户交互的形式出现的,因此又一个控制器来控制Modal弹层的出现和关闭。git

Modal组件的实现

静态组件

首先来思考如何实现静态组件部分的代码。es6

先在components下面建立咱们的modal组件结构。github

  • -components/
  • -modal/
    • -modal.js
    • -modal.scss

这里样式文件使用scss,若是不熟悉的同窗可使用css代替或者先学习一下scss语法规则。web

modal.js中建立出组件的基础部分。

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import './modal.scss';

export default class Modal extends Component {
	constructor(props) {
		super(props);
	}
	render() {
		return (
			<div>Modal</div>
		);
	}
}

Modal.propTypes = {};
Modal.defaultProps = {};

接下来分析咱们的组件都须要预留哪些接口:

  1. 开关状态isOpen
  2. Modal标题title
  3. Modal主体内容children
  4. Modal类名className
  5. 点击黑色区域是否能够关闭maskClosable
  6. 关闭按钮文案 cancelText
  7. 确认按钮文案 okText
  8. 关闭按钮回调函数 onCancel
  9. 确认按钮回调函数 onOk

目前能想到的接口有这些,接下来咱们能够补充一下咱们的代码。

// 刚才的代码部分
Modal.propTypes = {
	isOpen: PropTypes.bool.isRequired,
	title: PropTypes.string.isRequired,
	children: PropTypes.oneOfType([PropTypes.element, PropTypes.string]).isRequired,
	className: PropTypes.string,
	maskClosable: PropTypes.bool,
	onCancel: PropTypes.func,
	onOk: PropTypes.func,
	okText: PropTypes.string,
	cancelText: PropTypes.string
};

Modal.defaultProps = {
	className: '',
	maskClosable: true,
	onCancel: () => {},
	onOk: () => {},
	okText: 'OK',
	cancelText: 'Cancel'
};

定义好接口以后,咱们能够根据咱们的接口来完善一下Modal组件。

export default class Modal extends Component {
	constructor(props) {
		super(props);
		this.state = {
			isOpen: props.isOpen || false
		};
	}
	componentWillReceiveProps(nextProps) {
		if('isOpen' in nextProps) {
		  this.setState({
			isOpen: nextProps.isOpen
		  });
		}
  }
	render() {
		const {
			title,
			children,
			className,
			okText,
			cancelText,
			onOk,
			onCancel,
			maskClosable
		} = this.props;
		return (
			<div className={`mocal-container ${className}`}>
				<div className="modal-body">
					<div className={`modal-title ${type}`}>{title}</div>
                <div className="modal-content">{children}</div>
                <div className="modal-footer">
                  	<button className="ok-btn" onClick={onOk}>{okText}</button>
                  	<button className="cancel-btn" onClick={onCancel}>{cancelText}</button>
                </div>
				</div>
			</div>
		);
	}
}

接下来是Modal组件的样式:

.modal-container {
	background-color: rgba(33, 33, 33, .4);
	position: fixed;
	top: 0;
	left: 0;
	right: 0;
	bottom: 0;
	opacity: 1;
	.modal-body {
		background-color: #fff;
		border-radius: 5px;
		padding: 30px;
		width: 400px;
		position: absolute;
		left: 50%;
		top: 40%;
		transform: translate3d(-50%, -50%, 0);
		.modal-title {
			text-align: center;
			font-size: 18px;
			font-weight: bold;
		}
		.modal-content {
			min-height: 100px;
		}
		.modal-footer {
			text-align: center;
			button {
				margin: 0 20px;
				padding: 8px 27px;
				font-size: 16px;
				border-radius: 2px;
				background-color: #ffd900;
				border: 0;
				outline: none;
				&:hover {
					cursor: pointer;
					background-color: #fff000;
				}
			}
		}
	}
}

基础部分写完以后,咱们能够来验证一下本身的组件是否可以正常运行了。

咱们在直接在containers里面的hello里面引入Modal测试便可:

import React, { Component } from 'react';
import Modal from 'components/modal';

export default class Hello extends Component {
	render() {
		return (
			<Modal
				title="Demo"
				okText="确认"
				cancelText="取消"
			>
				<div>Hello world!</div>
			</Modal>
		);
	}
}

node启动开发机,登陆到localhost:8088,能够看到咱们的组件运行良好:

可是彷佛仍是有一点瑕疵,咱们的Modal不可能只有一个状态,所以咱们须要一个type接口,来控制咱们显示哪种Modal,好比success、error等。

继续改造Modal.js

Modal.PropTypes = {
	// ...
	type: PropTypes.oneOf(['alert', 'confirm', 'error'])
};
Modal.defaultProps = {
	// ...
	type: 'alert',
};

咱们在scss中稍微改变一点样式,能让咱们分辨出来。 基本上都是使用特定的icon图片来做区分,这里为了简化代码量,直接使用emoji字符来代替了。

.modal-title {
	// ...
	&.error:before {
			content: '❌';
			display: inline-block;
      }
      &.success:before {
			content: '✔';
			color: rgb(75, 231, 14);
			display: inline-block;
      }
      &.confirm:before {
			content: '❓';
			display: inline-block;
      }
      &.alert:before {
			content: '❕';
			display: inline-block;
      }
}

如今在看咱们的组件,能够看到已经有区分度了:

正常状况下,咱们会继续细分不少东西,好比什么状况下不显示按钮组,什么状况下只显示确认按钮等。这里就不进行细分工做了。

挂载方法

Modal组件的骨架搭好以后,咱们能够开始考虑组件须要的方法了。

首先组件是要能够关闭的,而且咱们不管点击确认或者取消或者黑色弹层都要能够关闭组件。

并且当咱们组件打开的时候,须要给body加上类名,方便咱们以后的一切操做。

const modalOpenClass = 'modal-open';

const toggleBodyClass = isOpen => {
	const body = document.body;
	if(isOpen) {
		body.classList.add(modalOpenClass);
	} else {
		body.classList.remove(modalOpenClass);
	}
}

export default class Modal extends Component {
	/// ...
	constructor(props) {
		// ...
		toggleBodyClass(props.isOpen);
	}
	// 关闭弹层函数
	close() {
		this.setState() {
			isOpen: false
		};
		toggleBodyClass(false);
	}
	// 点击确认回调函数
	onOkClick() {
		this.props.onOk();
		this.close();
	}
	// 点击取消的回调函数
	onCancelClick() {
		this.props.onCancel();
		this.close();
	}
	// ...
}

这些函数由于都要绑定到dom节点上,所以要提早绑定this,所以咱们能够写一个工具函数,建立一个lib文件夹,在lib下建立一个util.js文件。

// lib/util
export default {
	bindMethods(methods, obj) {
		methods.forEach(func => {
			if(typeof func === 'function') {
				obj[func] = obj[func].bind(this);
			}
		})
	}
}

而后在咱们的Modal组件中引入util文件,绑定函数的this。

// Modal.js
import util from 'lib/util';

// ...
constructor(props) {
	// ...
	util.bindMethods(['onCancelClick', 'onOkClick', 'close'], this);
}
// ...

而后咱们就能够将刚才的点击函数都替换掉:

render() {
	// ...
	return (
			<div className={`mocal-container ${className}`} onClick={maskClosable ? this.close : () => {}}>
				<div className="modal-body">
					<div className={`modal-title ${type}`}>{title}</div>
                <div className="modal-content">{children}</div>
                <div className="modal-footer">
                  	<button className="ok-btn" onClick={this.onOkClick}>{okText}</button>
                  	<button className="cancel-btn" onClick={this.onCancelClick}>{cancelText}</button>
                </div>
				</div>
			</div>
		);
}

去实验一下代码,发现确实能够关闭了。

控制器

Modal组件主体部分写完以后,咱们还要考虑考虑实际业务场景。

咱们都知道React是一个组件化的框架,咱们写好这个Modal组件后,不多是将这个组件嵌套在其余组件内部使用的,而是要直接在body下面占满全屏显示,因此写到这里为止是确定不够的。

而且在网站中,通常都是有一个按钮,当用户点击以后,才弹出Modal提示用户。

所以,咱们如今这种经过组件调用的方式是确定不行的,所以还要对这个Modal组件进行封装。

modal目录下建立一个index.js文件,表明咱们整个Modal组件的入口文件。

而后在index.js中书写咱们的主要控制器代码:

// index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Modal from './modal';

const show = (props) => {
	let component = null;
	const div = document.createElement('div');
	document.body.appendChild(div);

	const onClose = () => {
		ReactDOM.unmountComponentAtNode(div);
		document.body.removeChild(div);

		if(typeof props.onClose === 'function') {
			props.onClose();
		}
	}

	ReactDOM.render(
		<Modal
			{...props}
			onClose={onClose}
			ref={c => component = c}
			isOpen
		>{props.content}</Modal>,
		div
	);
	return () => component.close();
}

const ModalBox = {};
ModalBox.confirm = (props) => show({
	...props,
	type: 'confirm'
});

ModalBox.alert = (props) => show({
	...props,
	type: 'alert'
});

ModalBox.error = (props) => show({
	...props,
	type: 'error'
});

ModalBox.success = (props) => show({
	...props,
	type: 'success'
});

export default ModalBox;

这段控制器的代码比较简单。

show函数用来控制Modal组件的显示,当show以后,在body下面建立一个div,而后将Modal组件熏染到这个div下面,而且在删除的时候一块儿将div和Modal组件都删除掉。

ModalBox就负责咱们平时动态调用,根据咱们传入不一样的type值而显示不一样type的Modal组件。

如今咱们能够去改造一下container的入口文件了:

// hello.js

import React, { Component } from 'react';
import Modal from 'components/modal';

export default class Hello extends Component {
	render() {
		return (
			<div>
			<button onClick={() => Modal.confirm({
				title: 'Demo',
				content: 'Hello world!',
				okText: '确认',
				cancelText: '取消',
				onOk: () => console.log('ok'),
				onCancel: () => console.log('cancel')
			})}>click me!</button>
		</div>
		);
	}
}

到此为止,咱们点击click me的按钮以后,能够正常显示和关闭Modal组件了,而且点击确认和取消按钮的时候,都会调用相对应的回调函数来显示'ok' 'cancel'字样。

动画效果

生硬的Modal组件天然不是咱们最终追求的效果,因此咱们还要加上最后一个部分:动画效果。

React实现动画的方式有不少,可是总结起来可能只有两种:

  1. 使用css3实现动画。
  2. 根据react的状态管理利用js实现动画。

在复杂动画的状况下,通常选择第二种,所以我这里也是使用第三方react动画库来实现Modal的动画效果。

考虑到动画结束,删除组件以后还应该有一个回调函数,所以这里采用的是react-motion动画库,而不是常见的CSSTransitionGroup动画库。

在增长动画效果以前,咱们要增长一个刚才提到的动画结束以后的回调函数,所以还须要增长一个接口。

onRest: PropTypes.func

而且将这个接口的默认值改成空函数:

onRest: () => {}

这里就不介绍具体的react-motion的使用方法了,直接展现最终的代码:

import { Motion, spring, presets } from 'react-motion';

export default class Modal extends Component {
	constructor(props) {
		// ...
		util.bindMethods(['onCancelClick', 'onOkClick', 'close', 'onRest'], this);
	}
	// ...
	// 动画结束以后的回调函数
	onRest() {
		const { isOpen } = this.state;
		if(!isOpen) {
			this.props.onClose();
		}
		this.props.onRest();
  }
  render() {
	// ...
	return (
		<Motion
			defaultStyle={{
				opacity: 0.8,
				scale: 0.8
			}}
			style={{
				opacity: spring(isOpen ? 1 : 0, presets.stiff),
				scale: spring(isOpen ? 1 : 0.8, presets.stiff)
			}}
			onRest={this.onRest}
		>
			{
				 ({
					opacity,
					scale
				}) => (
					<div
						className={`modal-container ${className}`}
						style={{opacity}}
						onClick={maskClosable ? this.close : () => {}}
					>
						<div
							className="modal-body"
							style={{
								opacity,
								transform: `translate3d(-50%, -50%, 0) scale(${scale})`
							}}
						>
							<div className={`modal-title ${type}`}>{title}</div>
							<div className="modal-content">{children}</div>
							<div className="modal-footer">
								<button className="ok-btn" onClick={this.onOkClick}>{okText}</button>
								<button className="cancel-btn" onClick={this.onCancelClick}>{cancelText}</button>
							</div>
						</div>
						</div>
					)
				}
			</Motion>
		);
	}
}

到此为止,整个Modal组件就已经完成了,但愿这份demo对学习react的同窗有所帮助。

结语

在设计基础组件的时候,必定要尽量多的考虑业务场景,而后根据业务场景去设计接口,尽可能保证基础组件可以在全部的场景中均可以正常使用。

这份Demo是在React15.6.0版本下书写的,由于React已经升级到16版本,而且16增长了新的createPortal()方法,因此Modal组件的实现方式会有所变化,具体的实现方法在下一篇文章介绍。React15.6.0实现Modal弹层组件

代码地址以下:<br>http://www.demodashi.com/demo/12315.html

注:本文著做权归做者,由demo大师代发,拒绝转载,转载须要做者受权

相关文章
相关标签/搜索