React+TS免注册DOM页面dialog弹窗

背景

在日常的需求开发中我们经常需要用到弹窗,那么在我们构建弹窗时,在引用组件是都需要引入组件DOM,然后通过事件来控制组件的影藏显示,调用也不是很方便;

本组件使用事前注册DOM的方式,业务方在使用时刻直接调用暴露的方法即可,无需再引用DOM。

示例

image-20200929155048437

组件使用示例

import newDialog from "*./components/newDialog"
newDialog.open({
    title: "退出发布",
    content: '是否需要保存草稿箱?',
    btn: [{ text: "不保存" },{ text: "保存" }],
    callback: (event)=>{
        if(event.result == 2){
            //TODO
        }else { 
            //TODO
        }
    }
});

API

参数说明类型是否必填
title弹窗标题String
content弹窗展示内容String\ReactDom
btn弹窗按钮可配置一个或者两个Array
callback弹窗响应回调方法Function

弹窗按钮说明

//弹窗按钮是json数组配置的 最多配置两个
btn: [{ text: "不保存" },{ text: "保存" }],
//每个按钮只需要配置text按钮文案即可 样式和颜色都集成到css
//如果配置一个按钮就是底部长按钮 配置两个根据配置的数组顺序从左向右展示

回调参数

//点击按钮会返回回调数据并关闭弹出
{
  result: 1/2 //返回按钮的索引值
}

完整代码

js代码newDialog.tsx

'use strict';
import './newDialog.scss';
import * as React from 'react';
import cs from "classnames";
import * as ReactDOM from 'react-dom';
import { isIphoneX } from './../../util/util';
/** 
 * @description 解决移动端滚动穿透事件 
 **/
const ModalHelper = (function (bodyCls) {
    var scrollTop;
    return {
        afterOpen: function () {
            scrollTop = document.scrollingElement.scrollTop;
            document.body.classList.add(bodyCls);
            document.body.style.top = -scrollTop + 'px';
        },
        beforeClose: function () {
            document.body.classList.remove(bodyCls);
            // scrollTop lost after set position:fixed, restore it back.
            document.scrollingElement.scrollTop = scrollTop;
        }
    };
})('modal-open');

interface IAppProps {
    title?: string;
    content?: any;
    btn?: any;
    callback?: Function;
}

interface IAppState {
    showToast?: Boolean;
    title?: string;
    content?: any;
    btn?: any;
    callback?: Function;
    isIPhoneX?: Boolean;
}
let DialogDiv;
class Dialog extends React.Component<IAppProps, IAppState> {
    constructor(props: IAppProps) {
        super(props);
        this.state = {
            showToast: true,
            isIPhoneX: false
        };
    }
    componentWillMount() {
        let isIPhoneX = isIphoneX();
        this.setState({
            isIPhoneX
        });
    }
    static open = (options) => {
        const { ...props } = options || {};
        DialogDiv = document.createElement('div');
        setTimeout(()=>{
            document.body.appendChild(DialogDiv);
            ReactDOM.render(<Dialog {...props}/> ,DialogDiv); 
        },100);
        ModalHelper.afterOpen();
    }
    close = (param) => {
        let result = param +1;
        this.props.callback({ result: result });
        this.setState({
            showToast: false
        });
        document.body.removeChild(DialogDiv);
        ModalHelper.beforeClose();
    }
    render() {
        const { showToast, isIPhoneX } = this.state;
        const { title, content, btn } = this.props;
        let _twoBtn = btn.length > 1 ? true : false;
        return (
            <div
                className={cs('dialog-container', {
                    show: showToast
                })}>
                <div className="pay-box">
                    <div className="new-dialog-box">
                        <div className="dialog-title"> {title} </div>
                        <div className="dialog-content"> {content} </div>
                        <div className={cs('dialog-bottom flex', { isIPhoneX: isIPhoneX })}>
                            {btn.map((item, idx) => {
                                return (
                                    <div
                                        onClick={() => {
                                            this.close(idx);
                                            // item.callback();
                                        }}
                                        className={cs('dialog-btn', {
                                            margin_r: _twoBtn && idx == 0,
                                            default: _twoBtn && idx == 0
                                        })}>
                                        {item.text}
                                    </div>
                                );
                            })}
                        </div>
                    </div>
                </div>
            </div>
        )
    }
}
export default Dialog;

css代码newDialog.scss

$baseFontSize: 16px !default;

// pixels to rems
@function pxToRem($px) {
    @return $px / $baseFontSize * 1rem;
}

.dialog-container {

    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, .7);
    display: none;
    z-index: 9999;

    &.show {
        display: block;
    }
}

.new-dialog-box {
    width: 100%;
    min-height: pxToRem(195px);
    background: rgba(255, 255, 255, 1);
    position: absolute;
    box-sizing: border-box;
    margin: 0;
    bottom: 0;
    z-index: 9999;
    transition: all 0.3s;
    border-radius: pxToRem(8px) pxToRem(8px) 0 0;
    // display: flex;
    .dialog-title {
        font-size: pxToRem(19px);
        margin: pxToRem(25px) auto pxToRem(25px);
        text-align: center;
        font-family: PingFangSC-Medium, PingFang SC;
        font-weight: 500;
        color: rgba(51, 51, 51, 1);
        line-height: pxToRem(19px);
    }

    .dialog-content {
        padding: 0 pxToRem(25px);
        font-size: pxToRem(14px);
        font-family: PingFangSC-Regular, PingFang SC;
        color: rgba(51, 51, 51, 1);
        margin: 0 auto pxToRem(15px);
        text-align: center;
        color: rgba(0, 0, 0, 1);
        line-height: pxToRem(27px);
        min-height: pxToRem(50px);
    }

    .dialog-bottom {
        height: pxToRem(45px);
        padding: 0 pxToRem(20px);
        margin-bottom: pxToRem(15px);
        display: flex;
        &.isIPhoneX {
            margin-bottom: pxToRem(49px);
        }
    }

    .dialog-btn {
        // margin: 0 auto pxToRem(30px);
        // width: pxToRem(300px);
        flex: 1;
        height: pxToRem(45px);
        background-image: linear-gradient(-44deg, #FCAF3C 0%, #F25228 98%);
        box-shadow: 0 8px 14px 0 rgba(190,118,22,0.20);
        border-radius: pxToRem(45px);
        text-align: center;
        line-height: pxToRem(45px);
        font-size: pxToRem(15px);
        font-family: PingFangSC-Medium, PingFang SC;
        color: rgba(255, 255, 255, 1);

        &.margin_r {
            margin-right: pxToRem(24px);
        }

        &.default {
            background: #FFFFFF;
            border: 1px solid #E6E6E6;
            box-shadow: 0 10px 20px -8px rgba(173,173,173,0.30);
            border-radius: pxToRem(45px);
            font-size: pxToRem(15px);
            font-family: PingFang-SC-Medium, PingFang-SC;
            color: rgba(85, 85, 85, 1);
        }
    }
}

另外附上代码中iPhone X系列机型判断方法

export const isIphoneX = function() {
    // iPhone X、iPhone XS
    var isIPhoneX =
        /iphone/gi.test(window.navigator.userAgent) &&
        window.devicePixelRatio &&
        window.devicePixelRatio === 3 &&
        window.screen.width === 375 &&
        window.screen.height === 812;
    // iPhone XS Max
    var isIPhoneXSMax =
        /iphone/gi.test(window.navigator.userAgent) &&
        window.devicePixelRatio &&
        window.devicePixelRatio === 3 &&
        window.screen.width === 414 &&
        window.screen.height === 896;
    // iPhone XR
    var isIPhoneXR =
        /iphone/gi.test(window.navigator.userAgent) &&
        window.devicePixelRatio &&
        window.devicePixelRatio === 2 &&
        window.screen.width === 414 &&
        window.screen.height === 896;
    if (isIPhoneX || isIPhoneXSMax || isIPhoneXR) {
        return true;
    }
    return false;
}
阅读 931

推荐阅读
移动端技术
用户专栏

移动端技术汇总

1543 人关注
64 篇文章
专栏主页