• react18 hooks自定义移动端Popup弹窗组件RcPop


    基于React18 Hooks实现手机端弹框组件RcPop

    react-popup 基于react18+hook自定义多功能弹框组件。整合了msg/alert/dialog/toast及android/ios弹窗效果。支持20+自定义参数、组件式+函数式调用方式,全方位满足各种弹窗场景需求。

    引入组件

    在需要使用弹窗的页面引入组件。

    // 引入自定义组件
    import RcPop, { rcpop } from './components/rcpop'

    RcPop支持 组件式+函数式 两种调用方式。

    组件写法

    复制代码
    <RcPop
        visible={visible}
        title="标题"
        content="弹窗内容"
        type="android"
        shadeClose="false"
        closeable
        :btns="[
            {text: '取消', click: () => setVisible(false)},
            {text: '确认', style: {color: '#09f'}, click: handleOK},
        ]"
        @onOpen={handleOpen}
        @onClose={handleClose}
    />
        <div>这里是自定义弹窗内容,优先级高于content内容。div>
    RcPop>
    复制代码

    函数写法

    复制代码
    function handlePopup() {
        rcpop({
            title: '标题',
            content: `

    函数式调用:rcpop({...})

    `, btns: [ { text: '取消', click: () => { // 关闭弹窗 rcpop.close() } }, { text: '确认', style: {color: '#09f'}, click: () => { rcpop({ type: 'toast', icon: 'loading', content: '加载中...', opacity: .2, time: 2 }) } } ] }) }
    复制代码
    • msg类型

    • 自定义多按钮

    复制代码
    rcpop({
        title: '标题',
        content: `

    显示自定义弹窗内容

    `, btns: [ { text: '稍后提示' }, { text: '取消', click: () => rcpop.close() }, { text: '立即更新', style: {color: '#09f'}, click: () => { // ... } } ] })
    复制代码

    • ios弹窗类型

    • android弹窗类型

    • 长按/右键菜单

    • 自定义内容

    复制代码
    <RcPop
        visible={visible}
        closeable
        xposition="top"
        content="这里是内容信息"
        btns={[
            {text: '确认', style: {color: '#00d8ff'}, click: () => setVisible(false)},
        ]}
        onOpen={()=> {
            console.log('弹窗开启...')
        }}
        onClose={()=>{
            console.log('弹窗关闭...')
            setVisible(false)
        }}
        >
        <div style={{padding: '15px'}}>
            <img src={reactLogo} width="60" onClick={handleContextPopup} />
            <h3 style={{color:'#f60', 'paddingTop':'10px'}}>当 content 和 自定义插槽 内容同时存在,只显示插槽内容。h3>
        div>
    RcPop>
    复制代码
    复制代码
    function handleContextPopup(e) {
        let points = [e.clientX, e.clientY]
        rcpop({
            type: 'contextmenu',
            follow: points,
            opacity: 0,
            btns: [
                {text: '标记备注信息'},
                {
                    text: '删除',
                    style: {color:'#f00'},
                    click: () => {
                        rcpop.close()
                    }
                }
            ]
        })
    }
    复制代码

    这次主打的是学习 React Hooks 开发自定义弹窗,之前也有开发过类似的弹层组件。

    https://www.cnblogs.com/xiaoyan2017/p/14085142.html

    https://www.cnblogs.com/xiaoyan2017/p/11589149.html

    编码开发

    在components目录下新建rcpop文件夹。

    rcpop支持如下参数配置

    复制代码
    // 弹窗默认参数
    const defaultProps = {
        // 是否显示弹出层
        visible: false,
        // 弹窗唯一性标识
        id: null,
        // 弹窗标题
        title: '',
        // 弹窗内容
        content: '',
        // 弹窗类型(toast | footer | actionsheet | actionsheetPicker | ios | android | androidSheet | contextmenu)
        type: '',
        // toast图标(loading | success | fail)
        icon: '',
        // 是否显示遮罩层
        shade: true,
        // 点击遮罩层关闭
        shadeClose: true,
        // 遮罩透明度
        opacity: '',
        // 自定义遮罩层样式
        overlayStyle: {},
        // 是否圆角
        round: false,
        // 是否显示关闭图标
        closeable: false,
        // 关闭图标位置(left | right | top | bottom)
        closePosition: 'right',
        // 关闭图标颜色
        closeColor: '',
        // 动画类型(scaleIn | fadeIn | footer | fadeInUp | fadeInDown)
        anim: 'scaleIn',
        // 弹窗出现位置(top | right | bottom | left)
        position: '',
        // 长按/右键弹窗(坐标点)
        follow: null,
        // 弹窗关闭时长,单位秒
        time: 0,
        // 弹窗层级
        zIndex: 2023,
        // 弹窗按钮组(text | style | disabled | click)
        btns: null,
        // 指定挂载的节点(仅对标签组件有效)
        // teleport = () => document.body,
        teleport: null,
        // 弹窗打开回调
        onOpen: () => {},
        // 弹窗关闭回调
        onClose: () => {},
        // 点击遮罩层回调
        onClickOverlay: () => {},
        // 自定义样式
        customStyle: {},
        // 类名
        className: null,
        // 默认插槽内容
        children: null
    }
    复制代码

    弹窗组件模板

    复制代码
    const renderNode = () => {
        return (
            <div ref={ref} className={classNames('rc__popup', options.className, {'rc__popup-closed': closed})} id={options.id} style={{'display': !opened.current ? 'none' : undefined}}>
                {/* 遮罩层 */}
                { isTrue(options.shade) && <div className="rcpopup__overlay" onClick={handleShadeClick} style={{'opacity': options.opacity, 'zIndex': oIndex-1, ...options.overlayStyle}}>div> }
                {/* 窗体 */}
                <div className="rcpopup__wrap" style={{'zIndex': oIndex}}>
                    <div
                        ref={childRef}
                        className={classNames(
                            'rcpopup__child',
                            {
                                [`anim-${options.anim}`]: options.anim,
                                [`popupui__${options.type}`]: options.type,
                                'round': options.round
                            },
                            options.position
                        )}
                        style={popStyles}
                    >
                        { options.title && <div className="rcpopup__title">{options.title}div> }
                        { (options.type == 'toast' && options.icon) && <div className={classNames('rcpopup__toast', options.icon)} dangerouslySetInnerHTML={{__html: ToastIcon[options.icon]}}>div> }
                        {/* 内容 */}
                        { options.children ? <div className="rcpopup__content">{options.children}div> : options.content ? <div className="rcpopup__content" dangerouslySetInnerHTML={{__html: options.content}}>div> : null }
                        {/* 按钮组 */}
                        { options.btns && 
                            <div className="rcpopup__actions">
                                {
                                    options.btns.map((btn, index) => {
                                        return <span className={classNames('btn', {'btn-disabled': btn.disabled})} key={index} style={btn.style} dangerouslySetInnerHTML={{__html: btn.text}} onClick={e => handleActions(e, index)}>span>
                                    })
                                }
                            div>
                        }
                        { isTrue(options.closeable) && <div className={classNames('rcpopup__xclose', options.closePosition)} style={{'color': options.closeColor}} onClick={close}>div> }
                    div>
                div>
            div>
        )
    }
    复制代码

    完整代码块

    复制代码
    /**
     * @title    基于react18 hooks自定义移动端弹窗组件
     * @author   YXY  Q: 282310962
     * @date     2023/07/25
     */
    import { useState, useEffect, createRef, useRef, forwardRef, useImperativeHandle } from 'react'
    import { createPortal } from 'react-dom'
    import { createRoot } from 'react-dom/client'
    
    // ...
    
    const RcPop = forwardRef((props, ref) => {
        const mergeProps = {
            ...defaultProps,
            ...props
        }
        
        const [options, setOptions] = useState(mergeProps)
        const [oIndex, setOIndex] = useState(options.zIndex)
        const [closed, setClosed] = useState(false)
        const [followStyle, setFollowStyle] = useState({
            position: 'absolute',
            left: '-999px',
            top: '-999px'
        })
    
        const opened = useRef(false)
        const childRef = useRef()
        const stopTimer = useRef(null)
    
        const popStyles = options.follow ? { ...followStyle, ...options.customStyle } : { ...options.customStyle }
    
        const isTrue = (str) => /^true$/i.test(str)
    
        const ToastIcon = {
            loading: '',
            success: '',
            error: '',
            warning: '',
            info: '',
        }
    
        /**
         * 开启弹窗
         */
        function open(params) {
            params && setOptions({ ...options, ...params })
    
            if(options.type == 'toast') {
                options.time = options.time || 3
            }
            if(opened.current) return
            opened.current = true
            
            setOIndex(++index + options.zIndex)
            options.onOpen?.()
    
            // 右键/长按菜单
            if(options.follow) {
                setTimeout(() => {
                    let rcpop = childRef.current
                    let oW, oH, winW, winH, pos
    
                    oW = rcpop.clientWidth
                    oH = rcpop.clientHeight
                    winW = window.innerWidth
                    winH = window.innerHeight
                    pos = getPos(options.follow[0], options.follow[1], oW, oH, winW, winH)
    
                    setFollowStyle({
                        ...followStyle,
                        left: pos[0],
                        top: pos[1]
                    })
                })
            }
    
            if(options.time) {
                clearTimeout(stopTimer.current)
                stopTimer.current = setTimeout(() => {
                    close()
                }, options.time * 1000)
            }
        }
    
        /**
         * 关闭弹窗
         */
        function close() {
            if(!opened.current) return
            setClosed(true)
            setTimeout(() => {
                setClosed(false)
                opened.current = false
                
                options.onClose?.()
                clearTimeout(stopTimer.current)
            }, 200)
        }
    
        // 点击遮罩层
        function handleShadeClick(e) {
            options.onClickOverlay?.(e)
            if(isTrue(options.shadeClose)) {
                close()
            }
        }
    
        // 点击按钮组
        function handleActions(e, index) {
            let btn = options.btns[index]
            if(!btn.disabled) {
                btn?.click?.(e)
            }
        }
    
        // 抽离的React的classnames操作类
        function classNames() {
            var hasOwn = {}.hasOwnProperty
            var classes = []
            for (var i = 0; i < arguments.length; i++) {
                var arg = arguments[i]
                if (!arg) continue
                var argType = typeof arg
                if (argType === 'string' || argType === 'number') {
                    classes.push(arg)
                } else if (Array.isArray(arg) && arg.length) {
                    var inner = classNames.apply(null, arg)
                    if (inner) {
                        classes.push(inner)
                    }
                } else if (argType === 'object') {
                    for (var key in arg) {
                        if (hasOwn.call(arg, key) && arg[key]) {
                            classes.push(key)
                        }
                    }
                }
            }
            return classes.join(' ')
        }
    
        // 获取挂载节点
        function getTeleport(getContainer) {
            const container = typeof getContainer == 'function' ? getContainer() : getContainer
            return container || document.body
        }
        // 设置挂载节点
        function renderTeleport(getContainer, node) {
            if(getContainer) {
                const container = getTeleport(getContainer)
                return createPortal(node, container)
            }
            return node
        }
    
        // 获取弹窗坐标点
        function getPos(x, y, ow, oh, winW, winH) {
            let l = (x + ow) > winW ? x - ow : x;
            let t = (y + oh) > winH ? y - oh : y;
            return [l, t];
        }
    
        const renderNode = () => {
            return (
                
    {/* 遮罩层 */} { isTrue(options.shade) &&
    } {/* 窗体 */}
    <div ref={childRef} className={classNames( 'rcpopup__child', { [`anim-${options.anim}`]: options.anim, [`popupui__${options.type}`]: options.type, 'round': options.round }, options.position )} style={popStyles} > { options.title &&
    {options.title}
    } { (options.type == 'toast' && options.icon) &&
    } {/* 内容 */} {/*{ (options.children || options.content) &&
    {options.children || options.content}
    }
    */} { options.children ?
    {options.children}
    : options.content ?
    : null } {/* 按钮组 */} { options.btns &&
    { options.btns.map((btn, index) => { return handleActions(e, index)}> }) }
    } { isTrue(options.closeable) &&
    }
    ) } useEffect(() => { props.visible && open() !props.visible && close() }, [props.visible]) // 暴露指定的方法给父组件调用 useImperativeHandle(ref, () => ({ open, close })) return renderTeleport(options.teleport || mergeProps.teleport, renderNode()) })
    复制代码

    react动态设置className,于是抽离封装了classNames函数。

    复制代码
    // 抽离的React的classnames操作类
    function classNames() {
        var hasOwn = {}.hasOwnProperty
        var classes = []
        for (var i = 0; i < arguments.length; i++) {
            var arg = arguments[i]
            if (!arg) continue
            var argType = typeof arg
            if (argType === 'string' || argType === 'number') {
                classes.push(arg)
            } else if (Array.isArray(arg) && arg.length) {
                var inner = classNames.apply(null, arg)
                if (inner) {
                    classes.push(inner)
                }
            } else if (argType === 'object') {
                for (var key in arg) {
                    if (hasOwn.call(arg, key) && arg[key]) {
                        classes.push(key)
                    }
                }
            }
        }
        return classes.join(' ')
    }
    复制代码

    非常方便的实现各种动态操作className类。

    通过 createRoot 将弹窗组件挂载到body,实现函数式调用。

    复制代码
    /**
     * 函数式弹窗组件
     * rcpop({...}) | rcpop.close()
     */
    let popRef = createRef()
    function Popup(options = {}) {
        options.id = options.id || 'rcpopup-' + Math.floor(Math.random() * 10000)
    
        // 判断id唯一性
        let rnode = document.querySelector(`#${options.id}`)
        if(options.id && rnode) return
    
        const div = document.createElement('div')
        document.body.appendChild(div)
    
        const root = createRoot(div)
        root.render(
            <RcPop
                ref={popRef}
                visible={true}
                {...options}
                onClose={() => {
                    let node = document.querySelector(`#${options.id}`)
                    if(!node) return
                    root.unmount()
                    document.body.removeChild(div)
                }}
            />
        )
    
        return popRef
    }
    复制代码

    OK,以上就是react18 hook实现自定义弹窗的一些小分享,希望对大家有所帮助~~💪

     

  • 相关阅读:
    AspectJ切面自定义注解实现参数分组校验——基础概念(1)
    docker/云托管/serverless部署Node项目总结
    禁用AMQP配置中的明文身份验证机制(包含Springboot结果测试+踩坑)
    【ML】K-Means 聚类
    「 网络安全常用术语解读 」什么是0day、1day、nday漏洞
    投标之招标文件查看
    题目 1119: C语言训练-“水仙花数“问题1(python详解)——练气三层中期
    uniapp 配置 底部 TabBar
    Mendix:企业成功执行数字化转型的9个因素
    【Leetcode】 100. 相同的树
  • 原文地址:https://www.cnblogs.com/xiaoyan2017/p/17592708.html