基于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: `<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类型

  • 自定义多按钮

rcpop({
title: '标题',
content: `<div style="color:#f90">
<p>显示自定义弹窗内容</p>
</div>`,
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: '<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的更多相关文章

  1. Vue自定义Popup弹窗组件|vue仿ios、微信弹窗|vue右键弹层

    基于vue.js构建的轻量级Vue移动端弹出框组件Vpopup vpopup 汇聚了有赞Vant.京东NutUI等Vue组件库的Msg消息框.Popup弹层.Dialog对话框.Toast弱提示.Ac ...

  2. svelte组件:Svelte自定义弹窗Popup组件|svelte移动端弹框组件

    基于Svelte3.x自定义多功能svPopup弹出框组件(组件式+函数式) 前几天有分享一个svelte自定义tabbar+navbar组件,今天继续带来svelte自定义弹窗组件. svPopup ...

  3. vue3系列:vue3.0自定义全局弹层V3Layer|vue3.x pc桌面端弹窗组件

    基于Vue3.0开发PC桌面端自定义对话框组件V3Layer. 前两天有分享一个vue3.0移动端弹出层组件,今天分享的是最新开发的vue3.0版pc端弹窗组件. V3Layer 一款使用vue3.0 ...

  4. 动态创建angular组件实现popup弹窗

    承接上文,本文将从一个基本的angular启动项目开始搭建一个具有基本功能.较通用.低耦合.可扩展的popup弹窗(脸红),主要分为以下几步: 基本项目结构搭建 弹窗服务 弹窗的引用对象 准备作为模板 ...

  5. 基于JQ的自定义弹窗组件

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  6. uni-app自定义Modal弹窗组件|仿ios、微信弹窗效果

    介绍 uniapp自定义弹窗组件uniPop,基于uni-app开发的自定义模态弹窗|msg信息框|alert对话框|confirm确认框|toast弱提示框 支持多种动画效果.多弹窗类型ios/an ...

  7. 百度智能小程序弹窗组件wcPop|智能小程序自定义model弹窗模板

    百度智能小程序自定义弹窗组件wcPop|百度小程序model对话框|智能小程序弹窗界面模板 最近百度也推出了自己的智能小程序,如是就赶紧去试了下,官方提供的api还不是狠完整.而且官方提供的弹窗组件也 ...

  8. uniapp自定义简单弹窗组件

    2.0(2019-08-31) 船新版本的信息弹窗组件 插件市场地址:http://ext.dcloud.net.cn/plugin?id=672 可以弹出很多条信息,并单独控制消失时间.点击消失. ...

  9. uniapp自定义picker城市多级联动组件

    uniapp自定义picker城市多级联动组件 支持多端--h5.app.微信小程序.支付宝小程序... 支持自定义配置picker插件级数 支持无限级 注意事项:插件传入数据格式为children树 ...

  10. 微信小程序弹窗组件

    概述 自己封装的一个比较简单微信弹窗小组件,主要就是教会大家对微信小组件的用法和理解,因为微信小程序对组件介绍特别少,所以我就把自己的理解分享给大家 详细 代码下载:http://www.demoda ...

随机推荐

  1. nuxt下运行项目时内存溢出(out of memory)的一种情况

    话不多说直接上代码: 如图,点红点的三行引入了一个组件,内容是同意注册协议的弹窗.但是在run dev的时候提示说内存溢出了(out of memory)...经过多方排查,定位到这个组件,警察叔叔就 ...

  2. 2023-01-08:小红定义一个仅有r、e、d三种字符的字符串中, 如果仅有一个长度不小于2的回文子串,那么这个字符串定义为“好串“。 给定一个正整数n,输出长度为n的好串有多少个。 结果对10^9

    2023-01-08:小红定义一个仅有r.e.d三种字符的字符串中, 如果仅有一个长度不小于2的回文子串,那么这个字符串定义为"好串". 给定一个正整数n,输出长度为n的好串有多少 ...

  3. 2021-02-18:给定一个字符串str,给定一个字符串类型的数组arr,出现的字符都是小写英文。arr每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来。返回需要至少多少张贴纸可以完成这个任务。例子:str= "babac",arr = {"ba","c","abcd"}。a + ba + c 3 abcd + abcd 2 abcd+ba 2。所以返回2。

    2021-02-18:给定一个字符串str,给定一个字符串类型的数组arr,出现的字符都是小写英文.arr每一个字符串,代表一张贴纸,你可以把单个字符剪开使用,目的是拼出str来.返回需要至少多少张贴 ...

  4. 2022-02-01:粉刷房子 II。 假如有一排房子,共 n 个,每个房子可以被粉刷成 k 种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。 当然,因为市场上不同颜色油漆的价

    2022-02-01:粉刷房子 II. 假如有一排房子,共 n 个,每个房子可以被粉刷成 k 种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同. 当然,因为市场上不同颜色油漆的价 ...

  5. vue全家桶进阶之路46:Vue3 Axios拦截器

    在Vue.js 3中,使用Axios与Vue.js 2.x中类似,但是需要进行一些修改和更新,下面是Vue.js 3中Axios的定义和使用方式: 首先,你需要安装Axios和Vue.js 3.x,可 ...

  6. POJ - 2251 地下城主

    You are trapped in a 3D dungeon and need to find the quickest way out! The dungeon is composed of un ...

  7. nodejs和npm升级版本

    由于服务器环境的不同可能需要根据实际情况升降对应的nodejs 及npm 版本,最简单的例子就是 npx 只适用于 npm 5+ 看想用npx 那不升级咋办呢,还有如error eslint@7.16 ...

  8. 各种版本的Linux 镜像下载网址

    今天发现Linux 镜像下载网址感觉很不错,分享给有需要的小伙伴们 访问地址 Linux操作系统各版本ISO镜像下载(包括oracle linux\redhat\centos\ubuntu\debia ...

  9. Python基础 - 赋值运算符

    以下假设变量a为10,变量b为20: 运算符 描述 实例 = 简单的赋值运算符 c = a + b 将 a + b 的运算结果赋值为 c += 加法赋值运算符 c += a 等效于 c = c + a ...

  10. GPT-4多态大模型研究

    1.概述 GPT-4是OpenAI最新的系统,能够产生更安全和更有用的回应.它是一个大型的多模态模型(接受图像和文本输入,输出文本),在各种专业和学术的基准测试中展现了人类水平的表现.例如,它在模拟的 ...