From ed81459aba1180aaf36ffa08080231919379cf0a Mon Sep 17 00:00:00 2001 From: minwe Date: Fri, 6 May 2016 11:17:13 +0800 Subject: [PATCH] refactor Modal render mode --- docs/modal/api.md | 18 ++- kitchen-sink/pages/ModalExample.js | 179 ++++++++++++++++++----------- src/js/index.js | 2 +- src/js/{ => modal}/Modal.js | 117 ++++++++++++------- src/js/modal/ModalPortal.js | 55 +++++++++ src/js/modal/index.js | 1 + 6 files changed, 262 insertions(+), 110 deletions(-) rename src/js/{ => modal}/Modal.js (80%) create mode 100644 src/js/modal/ModalPortal.js create mode 100644 src/js/modal/index.js diff --git a/docs/modal/api.md b/docs/modal/api.md index 8d7dde4..46a6eab 100644 --- a/docs/modal/api.md +++ b/docs/modal/api.md @@ -17,6 +17,12 @@ Modal 组件,用于弹出内容,实现 alert、confirm、prompt、loading、 Modal 形式,不设置时为普通的模态窗口。 +##### `isOpen` + +> PropType: `bool` + +Modal 是否为打开状态。 + ##### `title` > PropType: `node` @@ -53,6 +59,8 @@ Modal 标题。 用户点击「确定」或「取消」按钮时的处理函数。 +`role` 为 `prompt` 时,通过该函数的返回值可以控制按钮点击时是否关闭 Modal。 + ##### `onOpen` > PropType: `func` @@ -65,10 +73,18 @@ Modal 打开时的回调函数。 Modal 关闭以后的回调函数。 +##### `onRequestClose` + +> PropType: `func` + +用户请求关闭操作时的处理函数。 + #### 方法 -调用方式见示例代码。 +~~调用方式见示例代码。~~ + +**Modal 实现方式调整,请勿直接调用其方法。** ##### `.open()` diff --git a/kitchen-sink/pages/ModalExample.js b/kitchen-sink/pages/ModalExample.js index b2017b3..fd930cb 100644 --- a/kitchen-sink/pages/ModalExample.js +++ b/kitchen-sink/pages/ModalExample.js @@ -1,4 +1,5 @@ import React from 'react'; +import ReactDOM from 'react-dom'; import { Container, Group, @@ -9,40 +10,81 @@ import { } from 'amazeui-touch'; const ModalExample = React.createClass({ - open() { - this.refs.modal.open(); + getInitialState() { + return { + isModalOpen: false, + }; }, - close() { - this.refs.modal.close(); + openModal() { + this.setState({ + isModalOpen: true, + }) + }, + + closeModal() { + this.setState({ + isModalOpen: false, + }); + }, + + onOpen() { + console.log('modal open....'); + }, + + onClosed() { + console.log('modal closed....'); }, handleAction(data) { - let role = this.refs.modal.props.role; + let role = this.getModalRole(); // 确定和取消放在一起处理 // data 为 true 时为 `确定` if (role === 'confirm') { - console.log('你的选择是:「' + (data ? '确定' : '取消') + '」') + console.log('你的选择是:「' + (data ? '确定' : '取消') + '」') } else if (role === 'prompt') { + // `prompt` 类型支持通过返回值控制是否关闭 Modal + + // 点击取消时 data 的值为 null + + // 简单的验证逻辑 + // 仅适用于一个输入框的场景,多个输入框的 data 值为 `['', '', ...]` + if (data === '') { + console.error('赶紧交出来啊,不然...你懂的...'); + return false; // 点击确定时不关闭 Modal + } + console.log('输入的数据是:', data); + return true; // 点击确定时关闭 Modal } }, + getModalRole() { + return this.props.modalProps.role; + }, + render() { return (
- - {React.cloneElement(React.Children.only(this.props.children), { - ref: 'modal', - onAction: this.handleAction - })} + + {this.getModalRole() !== 'loading' && this.props.children} +
); } @@ -55,12 +97,13 @@ const ModalExamples = React.createClass({ - - - Modal 内容 - + + Hello, Modal 内容 @@ -69,28 +112,25 @@ const ModalExamples = React.createClass({ > - - 这一个 Alert 窗口。 - + 这一个 Alert 窗口。 - - - 这一个 Confirm 窗口。 - + 这一个 Confirm 窗口。 @@ -99,14 +139,13 @@ const ModalExamples = React.createClass({ > - - 输入你的 IQ 卡密码: - - + 输入你的 IQ 卡密码: + @@ -115,16 +154,15 @@ const ModalExamples = React.createClass({ > - -
- - -
-
+
+ + +
@@ -133,12 +171,11 @@ const ModalExamples = React.createClass({ > - - + modalProps={{ + title: '使劲加载中...', + role: 'loading' + }} + /> - -
- - 分享到 - 微信 - Twitter - -
-
+
+ + 分享到 + 微信 + + Twitter + +
- - -

为你封了国境
为你赦了罪
为你撤了历史记载
为你涂了装扮
为你喝了醉
为你建了城池围墙
一颗热的心穿着冰冷外衣
一张白的脸漆上多少褪色的情节
在我的空虚身体里面
爱上哪个肤浅的王位
在你的空虚宝座里面
爱过什麽女爵的滋味
为你封了国境

为你赦了罪
为你撤了历史记载
一颗热的心
穿着冰冷外衣
一张白的脸
漆上多少褪色的情节
在我的空虚身体里面
爱上哪个肤浅的王位
在你的空虚宝座里面
爱过什麽女爵的滋味
在我的空虚身体里面
爱上哪个肤浅的王位
在你的空虚宝座里面
爱过什麽女爵的滋味

-
+ +

为你封了国境
为你赦了罪
为你撤了历史记载
为你涂了装扮
为你喝了醉
为你建了城池围墙
一颗热的心穿着冰冷外衣
一张白的脸漆上多少褪色的情节
在我的空虚身体里面
爱上哪个肤浅的王位
在你的空虚宝座里面
爱过什麽女爵的滋味
为你封了国境 +

为你赦了罪
为你撤了历史记载
一颗热的心
穿着冰冷外衣
一张白的脸
漆上多少褪色的情节
在我的空虚身体里面
爱上哪个肤浅的王位
在你的空虚宝座里面
爱过什麽女爵的滋味
在我的空虚身体里面
爱上哪个肤浅的王位
在你的空虚宝座里面
爱过什麽女爵的滋味 +

diff --git a/src/js/index.js b/src/js/index.js index 7ff15b2..8b287ec 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -21,7 +21,7 @@ export {default as Icon} from './Icon'; export {default as Field} from './Field'; export {default as List} from './List'; export {default as Loader} from './Loader'; -export {default as Modal} from './Modal'; +export {default as Modal} from './modal'; export {default as NavBar} from './NavBar'; export {default as Notification} from './Notification'; export {default as OffCanvas} from './OffCanvas'; diff --git a/src/js/Modal.js b/src/js/modal/Modal.js similarity index 80% rename from src/js/Modal.js rename to src/js/modal/Modal.js index deb1fa0..a83710b 100644 --- a/src/js/Modal.js +++ b/src/js/modal/Modal.js @@ -2,36 +2,41 @@ * @see https://github.com/yuanyan/boron */ -import React from 'react'; +import React, { + PropTypes, + createClass, +} from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; import CSSTransitionGroup from 'react-addons-css-transition-group'; -import ClassNameMixin from './mixins/ClassNameMixin'; -import CSSCore from './utils/CSSCore'; -import Events from './utils/Events'; -import TransitionEvents from './utils/TransitionEvents'; -import Button from './Button'; -import Icon from './Icon'; -import Loader from './Loader'; +import ClassNameMixin from '../mixins/ClassNameMixin'; +import TransitionEvents from '../utils/TransitionEvents'; +import Button from '../Button'; +import Icon from '../Icon'; +import Loader from '../Loader'; // MUST be equal to $modal-duration in _modal.scss const TRANSITION_TIMEOUT = 300; -const Modal = React.createClass({ +function noop() { +} + +const Modal = createClass({ mixins: [ClassNameMixin], propTypes: { - classPrefix: React.PropTypes.string, - role: React.PropTypes.oneOf(['alert', 'confirm', 'prompt', 'loading', + classPrefix: PropTypes.string, + role: PropTypes.oneOf(['alert', 'confirm', 'prompt', 'loading', 'actions', 'popup']), - title: React.PropTypes.node, - confirmText: React.PropTypes.string, - cancelText: React.PropTypes.string, - closeBtn: React.PropTypes.bool, - closeViaBackdrop: React.PropTypes.bool, - onAction: React.PropTypes.func, - onOpen: React.PropTypes.func, - onClosed: React.PropTypes.func, + title: PropTypes.node, + confirmText: PropTypes.string, + cancelText: PropTypes.string, + closeBtn: PropTypes.bool, + closeViaBackdrop: PropTypes.bool, + onAction: PropTypes.func, + onOpen: PropTypes.func, + onClosed: PropTypes.func, + onRequestClose: PropTypes.func, }, getDefaultProps() { @@ -40,12 +45,10 @@ const Modal = React.createClass({ confirmText: '确定', cancelText: '取消', closeBtn: true, - onAction: () => { - }, - onOpen: () => { - }, - onClosed: () => { - }, + onAction: noop, + onOpen: noop, + onClosed: noop, + onRequestClose: noop, }; }, @@ -56,16 +59,24 @@ const Modal = React.createClass({ }; }, - isClosed() { - return this.state.closed; + componentDidMount() { + if (this.props.isOpen) { + this.open(); + } }, - handleBackdropClick(e) { - if (e.target !== e.currentTarget || !this.props.closeViaBackdrop) { - return; + componentWillReceiveProps(nextProps) { + let isOpen = this.props.isOpen; + + if (!isOpen && nextProps.isOpen) { + this.open(); + } else if (isOpen && !nextProps.isOpen) { + this.close(); } + }, - this.close(); + isClosed() { + return this.state.closed; }, isPopup() { @@ -91,13 +102,33 @@ const Modal = React.createClass({ return (data.length === 0) ? null : ((data.length === 1) ? data[0] : data); }, - handleSelect(data, e) { - if (this.props.role === 'prompt' && data) { + // data === null: prompt -> canceled + // data === true: confirm -> confirmed + // data === false: confirm -> canceled + handleAction(data, e) { + let { + role, + onAction, + } = this.props; + let willClose = true; + + if (role === 'prompt' && data) { data = this.getFieldData(); + + willClose = onAction.call(this, data, e); + } else { + onAction.call(this, data, e); } - this.close(); - this.props.onAction.call(this, data, e); + willClose && this.requestClose(e); + }, + + handleBackdropClick(e) { + if (e.target !== e.currentTarget || !this.props.closeViaBackdrop) { + return; + } + + this.requestClose(e); }, open() { @@ -111,6 +142,7 @@ const Modal = React.createClass({ } }, + // Only for instance self calling close() { if (this.isClosed() || this.state.isClosing) { return; @@ -121,6 +153,11 @@ const Modal = React.createClass({ }); }, + // for user actions + requestClose(e) { + this.props.onRequestClose(e); + }, + handleClosed() { this.setState({ closed: true, @@ -141,7 +178,7 @@ const Modal = React.createClass({ {this.props.children}
@@ -200,7 +237,7 @@ const Modal = React.createClass({ ) : null; @@ -234,7 +271,7 @@ const Modal = React.createClass({ buttons = ( {confirmText} @@ -247,7 +284,7 @@ const Modal = React.createClass({ return ( {text} diff --git a/src/js/modal/ModalPortal.js b/src/js/modal/ModalPortal.js new file mode 100644 index 0000000..80ec84d --- /dev/null +++ b/src/js/modal/ModalPortal.js @@ -0,0 +1,55 @@ +import React, {createClass} from 'react'; +import ReactDOM, { + unmountComponentAtNode, + unstable_renderSubtreeIntoContainer as renderSubtreeIntoContainer +} from 'react-dom'; +import CSSCore from '../utils/CSSCore'; +import Modal from './Modal'; + +// const bodyElement = canUseDOM ? document.body : {appendChild: () => {}}; +const body = document.body; +const bodyClassName = 'has-modal-open'; + +const ModalPortal = createClass({ + propTypes: { + isOpen: React.PropTypes.bool.isRequired, + }, + + getDefaultProps() { + return { + isOpen: false, + }; + }, + + componentDidMount() { + this.node = document.createElement('div'); + this.node.className = '__modal-portal'; + body.appendChild(this.node); + this.renderModal(this.props); + }, + + componentWillReceiveProps(nextProps) { + this.renderModal(nextProps); + }, + + componentWillUnmount() { + unmountComponentAtNode(this.node); + body.removeChild(this.node); + CSSCore.removeClass(body, bodyClassName); + }, + + renderModal(props) { + CSSCore[(props.isOpen ? 'add' : 'remove') + 'Class'](body, bodyClassName); + this.portal = renderSubtreeIntoContainer( + this, + , + this.node + ); + }, + + render() { + return null; + } +}); + +export default ModalPortal; diff --git a/src/js/modal/index.js b/src/js/modal/index.js new file mode 100644 index 0000000..0cbdbc0 --- /dev/null +++ b/src/js/modal/index.js @@ -0,0 +1 @@ +export default from './ModalPortal';