all articles

welogger dev note 1 - how to handle modals in react

2017-03-27 @sunderls

welogger js react modal

welogger.com正在如火如荼的开发中,其中遇到的一些问题和经验在这里做一些笔记,分享给大家。也许有不准确或者还有能做得更好的地方,欢迎评论指正。

1. 背景

welogger用的react.js做的前端渲染,涉及到比较多用户交互,其中包括弹层。 弹层个人分两种:

  1. 一种是覆盖全屏幕的(我称之为Modal),就是操作较为复杂占用地方比较多的弹层
  2. 一种是简单的没有太多操作的(我称之为Alert),比如系统默认的window.alert(), window.confirm(), window.prompt()

首先welogger将不使用系统默认的Alert弹窗,首先是不好看,其次是可控性太差,尤其是其中的渐变效果,和welogger本身的风格不搭。

看上去不难,就开始思考如何实现一个优雅的弹层组件了。

2. 搜索现有组件

作为前端工程师,想要做个什么东西的时候,先去搜一下,基本上应该有现成的了,看看可以就直接用。搜了下Modal,overlay, popup等关键词,得到了以下组件:

2.1 react-modal

react-modal是react.js官方的组件,直接来看用法:

....
<Modal
  isOpen={bool}
  onAfterOpen={afterOpenFn}
  onRequestClose={requestCloseFn}
  closeTimeoutMS={n}
  style={customStyle}
  contentLabel="Modal"
>
  <h1>Modal Content</h1>
  <p>Etc.</p>
</Modal>

Modal作为一个组件, isOpen来控制其显示与否。这个看上去比较中庸了。比如如果我要实现一个alert('some alert msg')的话,我需要一个<Alert/>, 其实现为包装上述实现,isOpen由props来传递:

<Alert show={this.props.isOpen} msg={this.prop.show} />

然后覆盖window.alert()(假设使用redux):

window.alert = (msg) => {
    dispatch({
        type: 'SHOW_ALERT',
        isOpen: true,
        msg
    });
}

这里问题来了,我需要维持一个isOpen的flag在store里面,这个很蛋疼啊,alert(), confirm(), prompt()这三个全局的通过上述包装还好,但是如果我有自己实现的多个modal(比如datePicker)的话,store里面就会出现类似以下内容:

alert:
    isOpen: true
    msg:
    cancelCallback
    confirmCallback
modalA:
    isOpen: true
    param1:
    callback1:
    callback2:
modalB:
    isOpen: true
    params1:
    callback1:
    callback2
...

我需要在jsx里面声明所有的modal,然后通过一个统一actionCreator: showModal(modalName, params),将变化传递给reducer进行处理。 这就显得很繁琐了。尤其是在于我们需要处理弹层栈(弹层上弹层依次出现,依次消失)的时候,modal的z-index需要在jsx中事先定义好,或者在组件中增加z-index的定义。 无论怎么搞,modal都出现了一个我们需要额外考虑于心的z-index问题,而modal本身是纯粹的简单的,我们不需要额外的脑力负担。

modal需要实现在根节点中事先列出来,感觉是一种浪费,因为一个页面并不是会展示所有的modal。如果modal太多,我可能会想要根据具体所需去加载modal,比如webpack的code splitting,但是以上方式感觉不太好实现(好实现么?欢迎提供更好的方法)

总之,虽然这可能适合react的范式,但是我个人不喜欢。

2.2 react-modal-dialog

react-modal-dialog貌似是个华人的作品。首先看demo,可以看到其支持了modal stack,第一印象还不错。再来看实际代码:

import React, {PropTypes} from 'react';
import {ModalContainer, ModalDialog} from 'react-modal-dialog';

class View extends React.Component {
  state = {
    isShowingModal: false,
  }
  handleClick = () => this.setState({isShowingModal: true})
  handleClose = () => this.setState({isShowingModal: false})
  render() {
    return <div onClick={this.handleClick}>
      {
        this.state.isShowingModal &&
        <ModalContainer onClose={this.handleClose}>
          <ModalDialog onClose={this.handleClose}>
            <h1>Dialog Content</h1>
            <p>More Content. Anything goes here</p>
          </ModalDialog>
        </ModalContainer>
      }
    </div>;
  }
}

和react-modal一样,modal的显示与否和层级由state进行控制,嗯,不太喜欢。

2.3 react-popup

react-popup 稍微有些变化了: 首先是一个Popup组件需要render出来, Popup对样式和按钮等进行了预先定义,可以自定义:

import React from 'react';
import ReactDom from 'react-dom';
import Popup from 'react-popup';

ReactDom.render(
    <Popup />,
    document.getElementById('popupContainer')
);

然后,通过Popup的静态方法创建或者显示:

// 显示alert
Popup.alert('Hello, look at me');

// 创建一个稍微复杂的弹层
Popup.create({
    title: null,
    content: 'Hello, look at me', // content可以传入一个component
    className: 'alert',
    buttons: {
        right: ['ok']
    }
});

个人而言,这个解决方案更加适合我,它没有用state来控制modal的现实,估计内部用了一个数组维护所有的modal,然后create或者其他事件的时候,重新render。但是在显示的时候不支持stack(始终只显示了一个modal),另外popup的自定义等略显啰嗦。

3 思考modal到底是什么

modal到底是什么,和普通component有啥区别? 经过思考,我发现modal唯一的特别的地方就是其全局性。例如以下情况:

  1. pageA用到了componentB
  2. componentB中用户点击的时候触发modalC
  3. modalC中有用户交互,然后modalC或者关闭然后更新componentB,或者触发modalD

首先,如果modalC是通用的,就不能在componentB中直接写<modalC show={true}/>,因为这样会导致modal的dom可能出现在任何地方,css可能会出问题(当然,通过一系列css hack说不定可以避免)。也就是说modal只能维护在另外一个公共的地方,这样modal之间的transition效果等也才能做出来。

对于state而言,相较于维护所有modal的显示状态而言,维护当前显示中的modal可能更加简单明了。这样可以理清楚modal的逻辑关系了

  1. state维护一个modal element array: []
  2. render的时候,按照数组的先后顺序render即可
  3. 当要弹出新modal的时候,创建element: <modal />,然后在其上做任何想做的事情,然后压入栈 []
  4. 当药关闭一个modal的时候,pop就ok了。

从实现上看,更加像react-popup

关于动画效果,一个modal的出场可能根据不同场景有不同的动画,所以这部分不应该在modal component自身进行定义,而应该是在创建element的时候添加。

5 具体实现

根据上述分析,简单做了个demo用的rr-modal 有兴趣可以看看,以下简单说明使用方法:

1.在createStore的时候,引入Modal.reducer

import * as Modal from './lib/modal';
const store = createStore(combineReducers({
  modals: Modal.reducer
}));

Modal.use(store);

2.在页面中render <Modals />,作为modal放置地点

<div className={styles.modals}>
    <Modals />
</div>

3.用show()dismiss()来控制modal的显示与否

import { show } from 'lib/modal';
export default class Input extends React.Component {
    showSelecter() {
        show(<ModalSelectName select={val => this.props.onChange(val)}/>);
    }
    render() {
        return <div onClick={this.showSelecter.bind(this)} className={styles.outer}>
            {this.props.value || this.props.placeholder}
        </div>
    }
}

具体实现效果demo: https://sunderls.github.io/rr-modal/dist/

6 关于动画

<Modals />是掌管所有modal的,所以动画也需要在这里添加:

class Modals extends React.Component {
    constructor(props) {
        super(props);
    }

    render() {
        return <div>{this.props.modals}</div>;
    }
}

根据上一篇关于transition的文章来看,明显应该在上述render方法中添加<ReactCSSTransitionGroup />。然后给show()dismiss()增加第二个参数 transition来指定渐变效果就ok了。

目前welogger采用的上述modal管理办法,再过一些时间如果确实没发现什么大问题的话,我再好好整理下rr-modal。