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

这篇具有很好参考价值的文章主要介绍了react18 hooks自定义移动端Popup弹窗组件RcPop。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

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

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

引入组件

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

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

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

react18 hooks自定义移动端Popup弹窗组件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: `<div style="padding:20px;">
            <p>函数式调用:<em style="color:#999;">rcpop({...})</em></p>
        </div>`,
        btns: [
            {
                text: '取消',
                click: () => {
                    // 关闭弹窗
                    rcpop.close()
                }
            },
            {
                text: '确认',
                style: {color: '#09f'},
                click: () => {
                    rcpop({
                        type: 'toast',
                        icon: 'loading',
                        content: '加载中...',
                        opacity: .2,
                        time: 2
                    })
                }
            }
        ]
    })
}
  • msg类型

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

  • 自定义多按钮

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

rcpop({
    title: '标题',
    content: `<div style="color:#f90">
        <p>显示自定义弹窗内容</p>
    </div>`,
    btns: [
        { text: '稍后提示' },
        { text: '取消', click: () => rcpop.close() },
        {
            text: '立即更新',
            style: {color: '#09f'},
            click: () => {
                // ...
            }
        }
    ]
})

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

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

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

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

  • ios弹窗类型

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

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

  • android弹窗类型

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

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

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

  • 长按/右键菜单

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

  • 自定义内容

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

<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

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

编码开发

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

在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: '<svg viewBox="25 25 50 50"><circle fill="none" cx="50" cy="50" r="20"></circle></svg>',
        success: '<svg viewBox="0 0 1024 1024"><path d="M512 85.333c235.648 0 426.667 191.019 426.667 426.667S747.648 938.667 512 938.667 85.333 747.648 85.333 512 276.352 85.333 512 85.333zm-74.965 550.4l-90.582-90.581a42.667 42.667 0 1 0-60.33 60.33l120.704 120.705a42.667 42.667 0 0 0 60.33 0L768.811 424.49a42.667 42.667 0 1 0-60.288-60.331L436.992 635.648z" /></svg>',
        error: '<svg viewBox="0 0 1024 1024"><path d="M512 85.333C276.352 85.333 85.333 276.352 85.333 512S276.352 938.667 512 938.667 938.667 747.648 938.667 512 747.648 85.333 512 85.333zm128.427 606.72l-129.75-129.749-129.066 129.024a35.968 35.968 0 1 1-50.902-50.901L459.733 511.36 329.301 380.928a35.968 35.968 0 1 1 50.859-50.944l130.475 130.475 129.706-129.75a35.968 35.968 0 1 1 50.944 50.902L561.536 511.36l129.75 129.75a35.968 35.968 0 1 1-50.902 50.943z" /></svg>',
        warning: '<svg viewBox="0 0 1024 1024"><path d="M512 941.12q-89.28 0-167.52-34.08t-136.32-92.16T116 678.08t-34.08-168T116 342.56t92.16-136.32 136.32-92.16T512 80t168 34.08 136.8 92.16 92.16 136.32 34.08 167.52-34.08 168-92.16 136.8T680 907.04t-168 34.08zM460.16 569.6q0 23.04 14.88 38.88T512 624.32t37.44-15.84 15.36-38.88V248q0-23.04-15.36-36.96T512 197.12t-37.44 14.4-15.36 37.44zM512 688.64q-27.84 0-47.52 19.68t-19.68 47.52 19.68 47.52T512 823.04t48-19.68 20.16-47.52T560 708.32t-48-19.68z"/></svg>',
        info: '<svg viewBox="0 0 1024 1024"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm84 343.1l-87 301.4c-4.8 17.2-7.2 28.6-7.2 33.9 0 3.1 1.3 6 3.8 8.7s5.2 4 8.1 4c4.8 0 9.6-2.1 14.4-6.4 12.7-10.5 28-29.4 45.8-56.8l14.4 8.5c-42.7 74.4-88 111.6-136.1 111.6-18.4 0-33-5.2-43.9-15.5-10.9-10.3-16.3-23.4-16.3-39.2 0-10.5 2.4-23.7 7.2-39.9l58.9-202.7c5.7-19.5 8.5-34.2 8.5-44.1 0-6.2-2.7-11.7-8.1-16.5-5.4-4.8-12.7-7.2-22-7.2-4.2 0-9.3.1-15.3.4l5.5-17L570.4 407H596v.1zm17.8-88.7c-12.2 12.2-26.9 18.2-44.1 18.2-17 0-31.5-6.1-43.7-18.2-12.2-12.2-18.2-26.9-18.2-44.1s6-31.9 18-44.1c12-12.1 26.6-18.2 43.9-18.2 17.5 0 32.3 6.1 44.3 18.2 12 12.2 18 26.9 18 44.1s-6.1 31.9-18.2 44.1z"/></svg>',
    }

    /**
     * 开启弹窗
     */
    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 (
            <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 || options.content) && <div className="rcpopup__content">{options.children || options.content}</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>
        )
    }

    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实现自定义弹窗的一些小分享,希望对大家有所帮助~~💪

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

 文章来源地址https://www.toymoban.com/news/detail-617324.html

到了这里,关于react18 hooks自定义移动端Popup弹窗组件RcPop的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • uniapp 封装弹窗组件(popup ,单选按钮,可以自选表单并提交表单)附有完整代码

     ↓界面结构如下 首先利用radio-group 中的label分成两部分,这样点单选按钮就可以出发整个label包裹的样式↓(点击label区域单选按钮亮起,触发radioChange方法改变radioState的状态) ↓  选中的效果可以参照↓ 完整代码如下↓ 如何使用该组件↓

    2024年02月16日
    浏览(39)
  • React18 hook学习笔记

    useState useState用于在函数组件中声明和管理状态 它接受初始状态,并返回一个状态变量和一个更新状态的函数 通过调用更新状态的函数,可以改变状态的值并触发组件的重新渲染 useEffect React的useEffect钩子可以让开发者在函数组件中管理副作用。 副作用操作是指那些与组件渲

    2024年02月14日
    浏览(40)
  • 【微信小程序】使用weui组件库来实现弹出一个确认的弹窗popup,其中包含图片和名称

    在微信小程序中,你可以使用weui组件库来实现弹出一个确认的popup,并在其中包含图片和名称。以下是一个示例代码: 在wxml文件中,添加一个按钮来触发弹出确认popup: 在wxss文件中,定义确认popup的样式: 在js文件中,编写相应的逻辑来显示和隐藏确认popup,并传递图片和名

    2024年02月17日
    浏览(63)
  • React的hooks---自定义hooks

    通过自定义 Hook,可以将组件逻辑提取到可重用的函数中,在 Hook 特性之前,React 中有两种流行的方式来共享组件之间的状态逻辑: render props 和 高阶组件 ,但此类解决方案会导致组件树的层级冗余等问题。而自定义 Hook 的使用可以很好的解决此类问题 自定义 Hook 是一个函数

    2024年02月15日
    浏览(32)
  • 1. 使用Popup组件自定义弹框提示页面

    在QT中,经常使用QMessageBox进行弹框的提示,而在QML中并没有这个功能,但是可以利用Popup组件进行自定义弹框的设计。 该组件可以理解为是一个空白区域,默认的visible属性是false,即不显示,需要用户手动设置其为true,才能显示出来。或者使用其自带的open()和close()函数打开

    2024年02月07日
    浏览(62)
  • react18-webchat网页聊天实例|React Hooks+Arco Design仿微信桌面端

    React18 Hooks+Arco-Design+Zustand 仿微信客户端聊天 ReactWebchat 。 react18-webchat 基于 react18+vite4.x+arco-design+zustand 等技术开发的一款仿制微信网页版聊天实战项目。实现 发送带有emoj消息文本、图片/视频预览、红包/朋友圈、局部模块化刷新/美化滚动条 等功能。 编辑器:vscode 技术栈:

    2024年02月09日
    浏览(48)
  • React自定义Hooks

    React自定义Hooks是一项强大而实用的功能,它可以帮助开发者将逻辑和状态从组件中抽离出来,提高组件的可复用性和逻辑抽象能力。本论文将介绍React自定义Hooks的定义、使用方法和设计原则,分析其对函数式组件的优化作用,并通过实例演示了如何使用自定义Hooks提高组件性

    2024年02月12日
    浏览(34)
  • react之自定义hooks

    原文合集地址如下,有需要的朋友可以关注 本文地址 合集地址 任何相对独立、复用性强的逻辑,都可以 extract 为自定义 Hook,自定义 Hook 是一种复用 React 的状态逻辑的函数。 自定义 Hook 的主要特点是: 抽象组件间的状态逻辑,方便复用 让功能组件更纯粹,更易于维护 自定义

    2024年02月12日
    浏览(43)
  • React---函数组件的常用hook

    2024年01月19日
    浏览(40)
  • 【《React Hooks实战》——指导你使用hook开发性能优秀可复用性高的React组件】

    使用React Hooks后,你很快就会发现,代码变得更具有组织性且更易于维护。React Hooks是旨在为用户提供跨组件的重用功能和共享功能的JavaScript函数。利用React Hooks, 可以将组件分成多个函数、管理状态和副作用,并且不必声明类即可调用React内置的功能。而且,上述所有的操作

    2024年02月14日
    浏览(35)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包