Skip to content

Commit

Permalink
feat: button组件异步点击事件自带loading状态
Browse files Browse the repository at this point in the history
  • Loading branch information
kongjing committed Aug 29, 2023
1 parent cd37209 commit a30f64d
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 6 deletions.
20 changes: 20 additions & 0 deletions packages/vantui/src/button/demo/demo1.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,33 @@ import { View } from '@tarojs/components'
import { Button } from '@antmjs/vantui'

export default function Demo() {
// 点击事件返回Promise,即可让按钮自带loading状态
const handle = async () => {
await timeout()
}

return (
<View>
<Button type="default">默认按钮</Button>
<Button type="primary">主要按钮</Button>
<Button type="info">信息按钮</Button>
<Button type="warning">警告按钮</Button>
<Button type="danger">危险按钮</Button>
<View></View>
<Button type="primary" onClick={handle}>
异步操作触发normal模式的loading
</Button>
<Button type="primary" onClick={handle} loadingMode="toast">
异步操作触发toast模式的loading
</Button>
</View>
)
}

function timeout() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(1)
}, 3000)
})
}
55 changes: 50 additions & 5 deletions packages/vantui/src/button/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import type { ButtonProps } from '../../types/button.d'
import { useState, useEffect, useCallback } from 'react'
import { pxTransform } from '@tarojs/taro'
import { Button as TaroButton, View } from '@tarojs/components'
import * as utils from '../wxs/utils'
import { Icon } from '../icon/index'
import { Loading } from '../loading/index'
import { Toast } from '../toast/index'
import * as computed from './wxs'

let index = 0

export function Button(props: ButtonProps) {
const {
type = 'default',
Expand All @@ -20,16 +24,56 @@ export function Button(props: ButtonProps) {
color,
loadingSize = pxTransform(40),
loadingType = 'circular',
loadingText,
loadingText = '加载中...',
icon,
classPrefix = 'van-icon',
onClick,
children,
style,
className,
loadingMode = 'normal',
...others
} = props

const [innerLoading, setInnerLoading] = useState<boolean | undefined>(false)
const [compIndex] = useState<number>(++index)

const toastId = `van-button-toast_${compIndex}`

useEffect(() => {
setInnerLoading(loading)
}, [loading])

useEffect(() => {
if (innerLoading && loadingMode === 'toast') {
Toast.loading({
selector: `#${toastId}`,
duration: 60 * 60,
message: loadingText,
loadingType: loadingType,
})
} else {
Toast.clear()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [innerLoading])

const _click = useCallback(
(e) => {
if (onClick && !loading) {
const res = onClick(e)
// @ts-ignore
if (res?.then && res?.catch) {
setInnerLoading(true)
res.finally(() => {
setInnerLoading(false)
})
}
}
},
[loading, onClick],
)

return (
<View
className={
Expand All @@ -42,10 +86,10 @@ export function Button(props: ButtonProps) {
round,
plain,
square,
loading,
loading: innerLoading,
disabled,
hairline,
unclickable: disabled || loading,
unclickable: disabled || innerLoading,
},
]) +
' ' +
Expand All @@ -60,13 +104,14 @@ export function Button(props: ButtonProps) {
style,
])}
>
<Toast id={toastId} />
<TaroButton
className="van-native-button"
disabled={disabled}
onClick={disabled || loading ? undefined : onClick}
onClick={disabled || innerLoading ? undefined : _click}
{...others}
></TaroButton>
{loading ? (
{innerLoading && loadingMode === 'normal' ? (
<View style="display: flex">
<Loading
className="loading-class"
Expand Down
103 changes: 103 additions & 0 deletions packages/vantui/src/wxs/renderNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// @ts-nocheck

import { JSX, ReactNode } from 'react'
import { render, unmountComponentAtNode } from '@tarojs/react'
import { document } from '@tarojs/runtime'
import Taro from '@tarojs/taro'

let id = 0
const renderNodes: Record<string, any[]> = {}
const timers: Record<string, Record<string, NodeJS.Timeout>> = {}

export function createDynamicNode<T extends Record<any, any>>({
element,
delay = 60,
idPrefix = 'common', // 'dialog'、'toast'、'notify'
}: {
element: (props: T) => JSX.Element | ReactNode
delay?: number
idPrefix: string
}) {
if (!idPrefix) return console.error('createDynamicNode: idPrefix is unset')
if (!renderNodes[idPrefix]) {
renderNodes[idPrefix] = []
}

if (!timers[idPrefix]) {
timers[idPrefix] = {}
}

const Com = element
const view = document.createElement('View')
id += 1
view.id = `${idPrefix || ''}_element_${id}`
const currentPages = Taro.getCurrentPages()
const currentPage = currentPages[currentPages.length - 1] // 获取当前页面对象
const path = currentPage.$taroPath

const clearTimer = () => {
const t = timers[idPrefix][view.id]
if (t) {
clearTimeout(t)
delete timers[idPrefix][view.id]
}
}
const destroy = () => {
for (let i = 0; i < renderNodes[idPrefix]?.length; i++) {
if (renderNodes[idPrefix] === view) {
renderNodes[idPrefix]?.splice(i, 1)
break
}
}
clearTimer()
try {
unmountComponentAtNode(view)
const pageElement = document.getElementById(path)
pageElement?.removeChild(view)
} catch (error) {}
}

const clear = () => {
for (const key in timers) {
if (key === idPrefix) {
const m = timers[key]

for (const k in m) {
const t = m[k]
clearTimeout(t)
}
}
}

renderNodes[idPrefix]?.map((view) => {
try {
unmountComponentAtNode(view)
const pageElement = document.getElementById(path)
pageElement?.removeChild(view)
} catch (error) {}
})

delete renderNodes[idPrefix]
}

return {
show: (props: T) => {
if (delay) {
clearTimer()
timers[idPrefix][view.id] = setTimeout(() => {
destroy()
}, delay)
}
const isRenderFlag = renderNodes[idPrefix]?.filter((item) => {
return item === view
})?.length
if (!!isRenderFlag) return
render(<Com {...(props ?? {})} />, view)
const pageElement = document.getElementById(path)
pageElement?.appendChild(view)
renderNodes[idPrefix].push(view)
},
hide: destroy,
clear: clear,
}
}
12 changes: 11 additions & 1 deletion packages/vantui/types/button.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { FunctionComponent, ReactNode } from 'react'
import { ButtonProps as TaroButtonProps } from '@tarojs/components'
import { ButtonProps as TaroButtonProps, ITouchEvent } from '@tarojs/components'

export interface ButtonProps extends Omit<TaroButtonProps, 'size' | 'type'> {
/**
Expand All @@ -15,11 +15,21 @@ export interface ButtonProps extends Omit<TaroButtonProps, 'size' | 'type'> {
loading?: boolean
hairline?: boolean
disabled?: boolean
/**
* @description loading的文案
* @default 加载中
*/
loadingText?: ReactNode
loadingSize?: string
loadingType?: 'spinner' | 'circular'
/**
* @description loading的模式,按钮内loading 或 Toast.loading
* @default normal
*/
loadingMode?: 'normal' | 'toast'
color?: string
children?: ReactNode
onClick?: (event: ITouchEvent) => void | Promise<any>
}

declare const Button: FunctionComponent<ButtonProps>
Expand Down

0 comments on commit a30f64d

Please sign in to comment.