react編寫可編輯標(biāo)題示例詳解
需求
因?yàn)樽约簱Q工作到了新公司,上周入職,以前沒有使用過(guò)react框架,雖然前面有學(xué)習(xí)過(guò)react,但是并沒有實(shí)踐經(jīng)驗(yàn)
這個(gè)需求最終的效果是和石墨標(biāo)題修改實(shí)現(xiàn)一樣的效果

初始需求
- 文案支持可編輯
- 用戶點(diǎn)擊位置即光標(biāo)定位處
- 超過(guò)50字讀的時(shí)候,超出部分進(jìn)行截?cái)?/li>
- 當(dāng)用戶把所有內(nèi)容刪除時(shí),失去焦點(diǎn)時(shí)文案設(shè)置為 “無(wú)文案”三個(gè)字
- 編輯區(qū)域隨著編輯內(nèi)容的寬度而變化,最大寬度1000px 500px
- 失去焦點(diǎn)時(shí)保存文案內(nèi)容
方案設(shè)計(jì)
在看到第一眼需求的時(shí)候,想到的時(shí)候用span和input進(jìn)行切換,但是這個(gè)肯定是滿足不了需求中第2點(diǎn),所以首先這個(gè)需求肯定不會(huì)是兩個(gè) 標(biāo)簽切換,只能一個(gè)標(biāo)簽承擔(dān)展示和編輯的功能,第一反應(yīng)是用html屬性contentEditable,就有了我的第一個(gè)套方案,后因?yàn)樾枨蟮牡谌c(diǎn)實(shí)現(xiàn)上存在問題,所以被迫換了方案二(使用input標(biāo)簽),下面我們?cè)敿?xì)說(shuō)說(shuō)為啥棄用方案1選用方案二以及在這過(guò)程中遇到的問題。
方案一 span + contentEditable
思路
- 利用h5提供contentEditble,可實(shí)現(xiàn)需求點(diǎn)的1/2/5
- 監(jiān)聽focus事件和input時(shí)間,可以實(shí)現(xiàn)需求點(diǎn)4
- 監(jiān)聽blur事件,可以實(shí)現(xiàn)需求點(diǎn)3
但是 需求點(diǎn)中的3點(diǎn),因?yàn)槭怯米謹(jǐn)?shù)做的截?cái)啵谶@個(gè)方案中是實(shí)現(xiàn)不了的,所以我給出的建議方案是編輯的時(shí)候不做截?cái)?,非編輯的時(shí)候做截?cái)喽危ㄊ欠袷ソ裹c(diǎn)可用作判斷是否為編輯態(tài)的依據(jù))
代碼如下
演示demo:
import React, { useState, useRef, useEffect } from 'react';
import ReactDom from 'react-dom';
interface EditTextProps {
text: string;
// 告知父組件文案已被修改
changeText?: (text: string) => void;
}
const EditText = function (props: EditTextProps) {
useEffect(() => {
setShowText(props.text);
}, [props.text]);
const [showText, setShowText] = useState('');
const [isBlank, setIsBlank] = useState(false);
const [isFocus, setIsFocus] = useState(false);
const textRef = useRef<HTMLDivElement>(null);
const onFocus = () => {
setIsFocus(true)
}
const onInput = () => {
// 避免失去焦點(diǎn)的時(shí)候,標(biāo)題區(qū)域明顯的閃動(dòng)
setIsBlank(!textRef.current?.innerHTML);
}
const onBlur = () => {
const newTitle = textRef.current?.innerHTML || '無(wú)標(biāo)題';
const oldTitle = props.text;
setIsFocus(false);
setIsBlank(false);
// 文案更新
if (newTitle !== oldTitle) {
props?.changeText(newTitle);
setShowText(getCharsByLength(newTitle, 50));
}
else {
// 文案不更新
setShowText(getCharsByLength(newTitle, 50));
if(textRef.current) {
textRef.current.innerHTML = getCharsByLength(newTitle, 50)
}
}
}
// 獲取前l(fā)ength個(gè)字符
const getCharsByLength = (title: string, length: number) => {
const titleLength = title.length;
// 假設(shè)都是非中文字符,一個(gè)中文字符的寬度可以顯示兩個(gè)非中文字符
let maxLength = length * 2;
const result = [];
for (let i = 0; i < titleLength; i++) {
const char = title[i];
// 中文字符寬度2,非中文字符寬度1
maxLength -= /[\u4e00-\u9fa5]/.test(char) ? 2 : 1;
result.push(char);
if (maxLength <= 0) {
break;
}
}
if (result.length < titleLength) {
result.push('...');
}
return result.join('');
};
return <div className="title">
{isFocus && isBlank ? <span className="title-blank">無(wú)標(biāo)題</span> : ''}
<span
className="title-text"
contentEditable
suppressContentEditableWarning
ref={textRef}
onFocus={onFocus}
onInput={onInput}
onBlur={onBlur}
>{showText}</span>
</div>;
};
在這個(gè)方案中遇到的問題
如果在用戶修改之前的文案就是【無(wú)標(biāo)題】,此時(shí)用戶刪除了文案所有的內(nèi)容【將文案置空】,此時(shí)失去焦點(diǎn),根據(jù)需求我們應(yīng)該展示【無(wú)標(biāo)題】,可是在代碼邏輯中 進(jìn)行了setShowText(getCharsByLength(newTitle, 50));的處理,在不斷試探中,發(fā)現(xiàn)修改前后的showText一摸一樣,無(wú)法觸發(fā)dom的更新,針對(duì)這個(gè)問題我找到了兩個(gè)解決方式
- 方式一 在不需要更新標(biāo)題,用戶觸發(fā)了失去焦點(diǎn),但是并沒有修改標(biāo)題時(shí),先把showText設(shè)置為空,在setTimeout中設(shè)置會(huì)以前的標(biāo)題。
嘗試了一下這個(gè)方案,從使用角度來(lái)說(shuō)并不會(huì)特別明顯的閃動(dòng)。不過(guò)個(gè)人覺得這個(gè)方案代碼看著很怪異
const onBlur = () => {
const newTitle = textRef.current?.innerHTML || '無(wú)標(biāo)題';
const oldTitle = props.text;
setIsFocus(false);
setIsBlank(false);
// 文案更新
if (newTitle !== oldTitle) {
props?.changeText(newTitle);
setShowText(getCharsByLength(newTitle, 50));
}
else {
// 文案不更新
setShowText('');
setTimeout(() => {
setShowText(getCharsByLength(newTitle, 50));
}, 0)
}
}
- 方式二 利用ref
const onBlur = () => {
const newTitle = textRef.current?.innerHTML || '無(wú)標(biāo)題';
const oldTitle = props.text;
setIsFocus(false);
setIsBlank(false);
// 文案更新
if (newTitle !== oldTitle) {
props?.changeText(newTitle);
setShowText(getCharsByLength(newTitle, 50));
}
else {
// 文案不更新
setShowText(getCharsByLength(newTitle, 50));
if(textRef.current) {
textRef.current.innerHTML = getCharsByLength(newTitle, 50)
}
}
}
存在的問題
- 無(wú)法用字?jǐn)?shù)做限制
- 如果用寬度做限制,可以出現(xiàn)截?cái)嗟男Ч?,但是?nèi)容無(wú)法滑動(dòng)
方案二 直接用input處理展示和編輯
采用修改input框樣式的方法,讓input展示和可編輯文案。整體的效果和文章開頭展示的效果一致。 canEdit這個(gè)參數(shù)時(shí)我后面加的,用來(lái)控制EditText組件是否可以編輯。遇到的問題見面后面。 演示demo:
import React, { useState, useEffect, useRef, useLayoutEffect } from 'react';
interface EditTextProps {
text: string;
canEdit?: boolean;
changeText?: (text: string) => void;
}
function EditText(props: EditTextProps) {
// 根據(jù)span獲取寬度
const witdthRef = useRef<HTMLDivElement>(null);
const [showText, setShowText] = useState('');
const [isFocus, setIsFocus] = useState(false);
const [inputWith, setInputWith] = useState(100);
const minTitleWidth = 70;
const maxTitleWidth = 500;
useEffect(() => {
setShowText(props.text);
}, [props.text]);
useLayoutEffect(() => {
dealInputWidth();
}, [showText]);
const dealInputWidth = () => {
const offsetWidth = witdthRef?.current?.offsetWidth || minTitleWidth;
// +5 防止出現(xiàn) 截?cái)?
const width = offsetWidth < maxTitleWidth ? offsetWidth + 5 : maxTitleWidth;
setInputWith(width);
};
const titleFocus = () => {
setIsFocus(true);
};
const titleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
const newTitle = e.target.value;
setShowText(newTitle);
};
const titleBlur = () => {
const newTitle = showText || '無(wú)標(biāo)題';
const oldTitle = props.text;
setIsFocus(false);
if (showText !== oldTitle) {
setShowText(newTitle);
setIsFocus(false);
if (props?.changeText) {
props.changeText(newTitle);
}
} else {
setIsFocus(false);
setShowText(newTitle);
}
};
return (
<div className='wrap'>
{props.canEdit ? (
<input
value={showText}
style={{ width: inputWith }}
onFocus={titleFocus}
onChange={titleInput}
onBlur={titleBlur}
className='input'
placeholder="無(wú)標(biāo)題"
/>
) : (
''
)}
{/* 為了計(jì)算文字的寬度 */}
<span ref={witdthRef} className={props.canEdit ? 'width' : 'text'}>
{showText}
</span>
</div>
);
}
踩到的坑
input自帶寬度,無(wú)法實(shí)現(xiàn)寬度隨著文案的改變而改變。
在方案一做出來(lái)后,就和UI進(jìn)行了溝通在【編輯的時(shí)候用字?jǐn)?shù)做截?cái)鄬?shí)現(xiàn)不了】,給出了一個(gè)建議的方案【編輯的時(shí)候不做截?cái)唷?,但是設(shè)計(jì)同學(xué)覺得不截?cái)嗟姆桨高^(guò)丑,,,,,然后她就說(shuō)能實(shí)現(xiàn) 【石墨標(biāo)題編輯】時(shí),類似的效果交互嗎???于是我就開啟了研究石墨的效果的征途中。
只發(fā)現(xiàn) 石墨用了一個(gè)input實(shí)現(xiàn)了不錯(cuò)的效果,input后面放了一個(gè)span標(biāo)簽,我體驗(yàn)的時(shí)候,一直在想為什么會(huì)有一個(gè)span標(biāo)簽?zāi)兀??(小朋友,是不是滿臉疑問)
直到我發(fā)現(xiàn)input自帶寬度,無(wú)法隨著內(nèi)容的寬度的改變而改變。此時(shí)才恍然大悟span標(biāo)簽的作用。
我也采用了利用span標(biāo)簽的寬度的方式來(lái)控input輸入內(nèi)容的寬度。
開玩笑,咋可能這么順利,我遇到了第二個(gè)問題
用useEffect 來(lái)監(jiān)控 witdthRef.current.offsetWidth時(shí),拿到的是上次文案的寬度 經(jīng)過(guò)查閱資料,我發(fā)現(xiàn)了useLayoutEffect這個(gè)hook,真香
以上就是react編寫可編輯標(biāo)題示例詳解的詳細(xì)內(nèi)容,更多關(guān)于react編寫可編輯標(biāo)題的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React路由動(dòng)畫切換實(shí)現(xiàn)過(guò)程詳解
這篇文章主要介紹了react-router 路由切換動(dòng)畫的實(shí)現(xiàn)示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2022-12-12
使用Webpack打包React項(xiàng)目的流程步驟
隨著React應(yīng)用日益復(fù)雜,開發(fā)者需要借助模塊打包工具來(lái)管理項(xiàng)目依賴、轉(zhuǎn)換代碼和優(yōu)化性能,Webpack是一款功能強(qiáng)大的模塊打包器,它可以將React項(xiàng)目中的JavaScript、CSS、圖片等資源打包成瀏覽器友好的文件,本文將全面介紹如何使用Webpack打包React項(xiàng)目2025-03-03
antd-react使用Select組件defaultValue踩的坑及解決
這篇文章主要介紹了antd-react使用Select組件defaultValue踩的坑及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05
React基礎(chǔ)-JSX的本質(zhì)-虛擬DOM的創(chuàng)建過(guò)程實(shí)例分析
這篇文章主要介紹了React基礎(chǔ)-JSX的本質(zhì)-虛擬DOM的創(chuàng)建過(guò)程,結(jié)合具體實(shí)例形式分析了虛擬dom的基本原理與實(shí)現(xiàn)方法,需要的朋友可以參考下2023-05-05
react-redux action傳參及多個(gè)state處理的實(shí)現(xiàn)
本文主要介紹了react-redux action傳參及多個(gè)state處理的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07
關(guān)于React Native報(bào)Cannot initialize a parameter of type''NSArra
這篇文章主要介紹了關(guān)于React Native報(bào)Cannot initialize a parameter of type'NSArray<id<RCTBridgeModule>>錯(cuò)誤,本文給大家分享解決方案,需要的朋友可以參考下2021-05-05

