函式式元件中實現Antd開啟Modal後其Input框自動聚焦(focus)到文字的最後

bleaka發表於2022-03-11

目前React使用函式式元件已經成為趨勢, 如何把React函式式元件用好, 提高效能, 從而實現業務需求也成為了一種能力的體現......咳咳咳, 進入正題:

現實場景需求

我想實現這一個需求, 父元件收到文字訊息後將其傳送給子元件, 子元件是一個Antd元件中的Modal, 裡面只有一個input輸入框, 子元件收到父元件傳過來的文字訊息後開啟Modal對話方塊, 其input輸入框中的預設值為父元件傳遞過來的文字訊息, 並且自動focus到文字訊息的最後, 從而方便使用者輸入, 當輸入完之後, 點選確定按鈕呼叫函式再把訊息傳送回去.

遇到的問題

怎麼說呢, 我本來是打算使用一個給input輸入框一個ref(inputRef), 然後當modal框開啟時, 使用useEffect副作用鉤子, 然後通過使用inputRef的foucs函式來實現自動聚焦, 然而想象是美好的, 現實是殘酷的, 如果這麼容易能解決我就不會寫部落格了, 先上錯誤程式碼:

import React, { useRef, useEffect } from 'react'
import { Input, Modal } from 'antd'
// 引入useSelector用於讀取redux store中儲存的資料, 引入useDispath用於分發資料
import { useSelector, useDispatch } from "react-redux"
// 引入action
import { change_input_modal_visible } from "../../redux/actions/visible"
const { TextArea } = Input


// props 傳遞過來的是傳送訊息的函式和文字編輯框已有的內容textContent
export default function InputModal(props) {
    const inputRef = useRef()
    // store dispatch
    const dispatch = useDispatch()
    // store裡儲存的資料, 來控制modal是否顯示, 父元件收到文字框編輯訊息後會改為true, 從而顯示modal對話方塊
    const inputModalVisible = useSelector(state => state.visible.inputModalVisible)

    // 如果inputModalVisible為true, 聚焦input框
    useEffect(() => {
        if(inputModalVisible) {
            inputRef.current.focus({
                cursor: 'end'
            })
        }
    }, [inputModalVisible, inputRef])

    const handleOk = () => {
        let textValue  = inputRef.current.resizableTextArea.props.value
        console.log(textValue)
        // 去除前後多餘的空格
        let res = textValue.trim()
        // 如果內容不為空才傳送給UE4程式, 具體傳送邏輯省略
        // 將modal對話方塊關閉
        dispatch(change_input_modal_visible(false));
    };
    
    // 取消傳送訊息
    const handleCancel = () => {
        // 將modal對話方塊關閉
        dispatch(change_input_modal_visible(false));
    };

    return (
        <div className='modal_wrapper'>
            <Modal
                centered
                closable={false}
                destroyOnClose
                title={null}
                visible={inputModalVisible}
                onOk={handleOk}
                onCancel={handleCancel}
                cancelText="取消"
                okText="確定"
            >
                <TextArea
                    showCount
                    maxLength={100}
                    placeholder="請輸入內容"
                    allowClear
                    defaultValue={props.textContent}
                    ref={inputRef}
                />
            </Modal>
        </div>
    )
}

但是bug隨之就來了:
image
原因是在Modal框的visible為false時, 網頁上根本不會載入Modal節點, 當然就獲取不到inputRef, inputRef.current的結果就為null, 下圖第一張圖為Modal框的visible為false時的DOM樹, 第二張圖為Modal框的visible為true時的DOM樹:
image
Modal框的visible為false時的DOM樹
image
Modal框的visible為true時的DOM樹
既然問題找到了, 那就提一下我目前的解決辦法吧!

解決辦法

我的解決辦法是利用ref的原理, 當input輸入框掛載時, 使用ref進行聚焦, 關鍵程式碼片段如下:

<TextArea
	ref={(input) => {
		if (input != null) {
			input.focus({
				cursor: 'end'
			});
		}
	}}
>

但是隨之還有一個問題, 我現在ref用來進行聚焦了, 我如何拿到input輸入框內的值呢? 我還要輸入框訊息傳送回去呢! 還好Input輸入框還有一個onChange函式, 我可以用這個來維護一個state來儲存在state中, 既然思路有了, 就上一下原始碼:

import React, { useState, useEffect } from 'react'
import { Input, Modal } from 'antd'
// 引入useSelector用於讀取redux store中儲存的資料, 引入useDispath用於分發資料
import { useSelector, useDispatch } from "react-redux"
// 引入action
import { change_input_modal_visible } from "../../redux/actions/visible"
const { TextArea } = Input


// props 傳遞過來的是傳送訊息的函式和UE4中文字編輯框已有的內容textContent
export default function InputModal(props) {
    // 在state中儲存目前輸入框內的內容, 初始化為空字串
    const [textValue, setTextValue] = useState('')
    // store dispatch
    const dispatch = useDispatch()
    // store裡儲存的資料, 來控制modal是否顯示, 父元件收到文字框編輯訊息後會改為true, 從而顯示modal對話方塊
    const inputModalVisible = useSelector(state => state.visible.inputModalVisible)

    // 當props中textContent發生變化時, 即收到文字編輯框內容訊息更新之後
    // 同步更新儲存在textValue中
    useEffect(() => {
        setTextValue(props.textContent)
    }, [props.textContent])

    const handleOk = () => {
        // 傳送訊息
        console.log(textValue)
        // 去除前後多餘的空格
        let res = textValue.trim()
        // 如果內容不為空才傳送給UE4程式, 具體傳送邏輯不再展示
        // 將modal對話方塊關閉
        dispatch(change_input_modal_visible(false));
    };
    
    // 取消傳送訊息
    const handleCancel = () => {
        // 將modal對話方塊關閉
        dispatch(change_input_modal_visible(false));
    };

    const handleChange = e => {
        // 當輸入框內容發生變化時, 同步給textValue
        setTextValue(e.target.value)
    }

    return (
        <>
            <Modal
                centered
                closable={false}
                destroyOnClose
                title={null}
                visible={inputModalVisible}
                onOk={handleOk}
                onCancel={handleCancel}
                cancelText="取消"
                okText="確定"
            >
                <TextArea
                    showCount
                    maxLength={100}
                    placeholder="請輸入內容"
                    allowClear
                    defaultValue={props.textContent}
                    ref={(input) => {
                        if (input != null) {
                            input.focus({
                                cursor: 'end'
                            });
                        }
                    }}
                    onChange={handleChange}
                />
            </Modal>
        </>
    )
}

結語

至此, 本篇結束, 如果大家有更好的方法, 希望大家提出來, 或者有不懂的也可以留言, 感謝!

新解決辦法(2022/3/11)

此解決辦法來自於此帖子, 就是給所有modal彈框或者需要展示初始化資料的子元件增加一個展示判斷,顯示時重新初始化就行了.
通俗的說, 就是給Modal框套一個三元表示式, 如果visible為true才載入Modal框,這樣的話我們不需要維護onChange和一個state, 大大提高了效能, 程式碼如下:

import React, { useRef, useEffect } from 'react'
import { Input, Modal } from 'antd'
// 引入useSelector用於讀取redux store中儲存的資料, 引入useDispath用於分發資料
import { useSelector, useDispatch } from "react-redux"
// 引入action
import { change_input_modal_visible } from "../../redux/actions/visible"
const { TextArea } = Input


// props 傳遞過來的是傳送訊息的函式和UE4中文字編輯框已有的內容textContent
export default function InputModal(props) {
    const inputRef = useRef()
    // store dispatch
    const dispatch = useDispatch()
    // store裡儲存的資料, 來控制modal是否顯示, 父元件收到文字框編輯訊息後會改為true, 從而顯示modal對話方塊
    const inputModalVisible = useSelector(state => state.visible.inputModalVisible)

    useEffect(() => {
        if(inputModalVisible) {
            inputRef.current.focus({
                cursor: 'end'
            })
        }

    }, [inputModalVisible, inputRef])
    

    const handleOk = () => {
        // 傳送訊息
        let textValue  = inputRef.current.resizableTextArea.props.value
        console.log(textValue)
        // 去除前後多餘的空格
        let res = textValue.trim()
        // 如果內容不為空才傳送訊息, 具體邏輯程式碼省略
        // 將modal對話方塊關閉
        dispatch(change_input_modal_visible(false));
    };
    
    // 取消傳送訊息
    const handleCancel = () => {
        // 將modal對話方塊關閉
        dispatch(change_input_modal_visible(false));
    };

    return (
        <>
            {inputModalVisible? <Modal
                centered
                closable={false}
                destroyOnClose
                title={null}
                visible={inputModalVisible}
                onOk={handleOk}
                onCancel={handleCancel}
                cancelText="取消"
                okText="確定"
            >
                <TextArea
                    showCount
                    maxLength={100}
                    placeholder="請輸入內容"
                    allowClear
                    defaultValue={props.textContent}
                    ref={inputRef}
                />
            </Modal> : null}
        </>
    )
}

相關文章