在學習 React 時,最快樂的莫過遇到淺白易吸收的好文章。
Joe Morgan 所寫的【How To Add Login Authentication to React Applications】就是一例。
參考它的流程,讓我想到可以利用它來設計一款問卷調整系統,App 架構和路由經整理後如下圖所示:
整體建立流程和原文相同,我僅列出額外延伸的程式說明。
Router v6 的 Switch 被 Routes 取代
把【切換】變更為【多路由】更為合乎其語意。
Router v6 的 component 被 element 取代
不論是【頁面 (Page, View render)還是【元件 (Component)】,在 HTML 裡都是【元素
(Element)】,這也是朝合乎語意的方向修改。
React v6 和原始文章的差異處 |
建構問卷頁面
參考許多問卷頁面,通常一個頁面裡只會有一個問題,所以設計上只需要一個表單頁面
Survey,內容 Survey-Container
元件則是一次填入一個問題,使用者答題後把答案記錄後再換下一個問題,以此類推。
先建立目錄:
\>mkdir src/components
使用 Visual Studio Code 建立
survey-container.js
1 2 3 4 5 6 7 8 9 10 11 | // ./src/components/survey-container.js import React from "react" export default function SurveyContainer(props){ return (<> <div> <h2>{props.title}</h2> </div> </>) } |
使用 Visual Studio Code 建立 survey.js
1 2 3 4 5 6 7 8 9 10 11 12 | // ./src/views/survey.js import React from "react" import SurveyContainer from "../components/survey-container" export default function Survey() { return <> <form> <SurveyContainer title="Topic"/> </form> </> } |
存檔後網站會進行熱更新,此時你在登入後會看到問卷頁面:
接下來,建立模擬來自後端的問卷題目清單,並且添加一個流水號變數依序提交給 SurveyContainer 元件,key 在這裡是必要的,因為 React 會視 key 值變化而刷新元件,就可以達到表單內容刷新的效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // ./src/views/survey.js import React from "react" import SurveyContainer from "../components/survey-container" const surveyItems = [ {id:"1",title:"Topic1",itemOptions:"optionA;optionB;optionC;optionD"}, {id:"2",title:"Topic2",itemOptions:"optionA;optionB;optionC;optionD"}, {id:"3",title:"Topic3",itemOptions:"optionA;optionB;optionC;optionD"} ] export default function Survey() { const [groupNum, setGroupNum] = React.useState(1) return <> <form> <SurveyContainer key={groupNum} {...surveyItems[groupNum - 1]} /> </form> </> } |
關閉 survey.js,對 SurveyContainer 元件添加對題目、選項的渲染,底下加上 <button> 做下一題的切換:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // ./src/views/survey-container.js import React from "react" export default function SurveyContainer(props){ const {id, title} = props const itemOptions = props.itemOptions.split(";") return (<> <div> <h2>{title}</h2> {itemOptions.map( (element, index) => { return ( <div key={element+index}> <label><input type="radio" name={id} value={index+1} />{element}</label> </div> ) })} <button>Next</button> </div> </>) } |
SurveyContainer 點擊 <button> 後會使 Survey 觸發變更題號事件,可以在 Survey 建立和 onClick 事件給 SurveyContainer 元件使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // ./src/views/survey.js export default function Survey() { const [groupNum, setGroupNum] = React.useState(1) const handleNextClick = (e) => { e.preventDefault() setGroupNum(prevGroupNum=>{ const newGroupNum = prevGroupNum+1 return newGroupNum }) } return <> <form> <SurveyContainer key={groupNum} handleNextClick={handleNextClick} {...surveyItems[groupNum - 1]} /> </form> </> } |
SurveyContainer 元件則可以在 props 取出 handleNextClick 並掛載使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // ./src/views/survey-container.js import React from "react" export default function SurveyContainer(props){ const {id, title, handleNextClick} = props const itemOptions = props.itemOptions.split(";") return (<> <div> <h2>{title}</h2> {itemOptions.map( (element, index) => { return ( <div key={element+index}> <label><input type="radio" name={id} value={index+1} />{element}</label> </div> ) })} <button onClick={handleNextClick}>Next</button> </div> </>) } |
到這個階段即可順利載入問卷題目及選項,而且按下 Next 也會跳下一題:
Survey 頁面設定【最後一題】旗標,讓 SurveyContainer 能依照此屬性變化顯示內容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // ./src/views/survey.js export default function Survey() { const [groupNum, setGroupNum] = React.useState(1) const [isLast, setIsLast] = React.useState(false) const handleNextClick = (e) => { e.preventDefault() setGroupNum(prevGroupNum=>{ const newGroupNum = prevGroupNum+1 if (surveyItems.length === newGroupNum) setIsLast(true) return newGroupNum }) } return <> <form> <SurveyContainer key={groupNum} handleNextClick={handleNextClick} isLast={isLast} {...surveyItems[groupNum - 1]} /> </form> </> } |
SurveyContainer 可以在最後一題時將 Next 字樣修改為 Finish,讓使用者知道已經到最後一題:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // ./src/views/survey-container.js export default function SurveyContainer(props){ const {id, title, isLast, handleNextClick} = props const itemOptions = props.itemOptions.split(";") return (<> <div> <h2>{title}</h2> {itemOptions.map( (element, index) => { return ( <div key={element+index}> <label><input type="radio" name={id} value={index+1} />{element}</label> </div> ) })} <button onClick={handleNextClick}>{isLast?"Finish":"Next"}</button> </div> </>) } |
此外,使用者在還沒答題前,Next 或 Finish 按鈕不應該出現,應該在使用者選擇答案項目後才能顯示。SurveyContainer 要加入 onChange 事件並對 <button> 進行顯示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // ./src/views/survey-container.js export default function SurveyContainer(props){ const [showClick, setShowClick] = React.useState(false) const {id, title, isLast, handleNextClick} = props const itemOptions = props.itemOptions.split(";") const handleOptionChange = (e) => { if (!showClick) setShowClick(!showClick) } return (<> <div> <h2>{title}</h2> {itemOptions.map( (element, index) => { return ( <div key={element+index}> <label><input type="radio" name={id} value={index+1} onChange={handleOptionChange} />{element}</label> </div> ) })} <button style={{display:showClick?"block":"none"}} onClick={handleNextClick}>{isLast?"Finish":"Next"}</button> </div> </>) } |
此時畫面變化如下:
使用者答題後要將答題內容保存,在 Survey 增加 ansers 狀態及 handleSetAnser 事件 ,並提交給 SurveyContainer 使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | // ./src/views/survey.js export default function Survey() { const [groupNum, setGroupNum] = React.useState(1) const [isLast, setIsLast] = React.useState(false) const [ansers, setAnsers] = React.useState([]) const handleNextClick = (e) => { e.preventDefault() setGroupNum(prevGroupNum=>{ const newGroupNum = prevGroupNum+1 if (surveyItems.length === newGroupNum) setIsLast(true) return newGroupNum }) } const handleSetAnser = (value) => { const newAnsers = [...ansers] newAnsers[groupNum-1] = value setAnsers(newAnsers) } return <> <form> <SurveyContainer key={groupNum} handleNextClick={handleNextClick} setAnser={handleSetAnser} isLast={isLast} {...surveyItems[groupNum - 1]} /> </form> </> } |
SurveyContainer 則在使用者選擇項目的當下儲存答案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // ./src/views/survey-container.js export default function SurveyContainer(props){ const [showClick, setShowClick] = React.useState(false) const {id, title, isLast, handleNextClick, setAnser} = props const itemOptions = props.itemOptions.split(";") const handleOptionChange = (e) => { if (!showClick) setShowClick(!showClick) setAnser(e.target.value) } return (<> <div> <h2>{title}</h2> {itemOptions.map( (element, index) => { return ( <div key={element+index}> <label><input type="radio" name={id} value={index+1} onChange={handleOptionChange} />{element}</label> </div> ) })} <button style={{display:showClick?"block":"none"}} onClick={handleNextClick}>{isLast?"Finish":"Next"}</button> </div> </>) } |
答題和使用者介面調整結束後,就要進行統計和顯示成績,Survey 增加結束旗標 finish,使用 useEffect 判斷 finish 後使用 useNavigate 跳轉到成績頁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | // ./src/views/survey.js import React from "react" import { useNavigate } from "react-router" import SurveyContainer from "../components/survey-container" const surveyItems = [ {id:"1",title:"Topic1",itemOptions:"optionA;optionB;optionC;optionD"}, {id:"2",title:"Topic2",itemOptions:"optionA;optionB;optionC;optionD"}, {id:"3",title:"Topic3",itemOptions:"optionA;optionB;optionC;optionD"} ] export default function Survey() { const [groupNum, setGroupNum] = React.useState(1) const [isLast, setIsLast] = React.useState(false) const [ansers, setAnsers] = React.useState([]) const [finished, setFinished] = React.useState(false) const navigate = useNavigate(); React.useEffect(()=>{ if (finished) { // calc score navigate('/score/100') } },[finished]) const handleNextClick = (e) => { e.preventDefault() setGroupNum(prevGroupNum=>{ let newGroupNum = prevGroupNum+1 if (surveyItems.length === newGroupNum) setIsLast(true) if (isLast) { setFinished(true) newGroupNum = prevGroupNum } return newGroupNum }) } const handleSetAnser = (value) => { const newAnsers = [...ansers] newAnsers[groupNum-1] = value setAnsers(newAnsers) } return <> <form> <SurveyContainer key={groupNum} handleNextClick={handleNextClick} setAnser={handleSetAnser} isLast={isLast} {...surveyItems[groupNum - 1]} /> </form> </> } |
newGroupNum 變為 let 宣告是因為避免超過總題數造成 SurveyContainer 無法讀取題目的異常,進而無法順利進行頁面跳轉,順利跳轉後如下圖顯示成績:
使用 Delphi DataSnap 建立 Token API
關於 DataSnap 建立的內容有很多,可以參考:
主程式內容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | unit ServerMethodsUnit1; interface uses SysUtils, Classes, DSServer, DBXJSON; type {$METHODINFO ON} TServerMethods1 = class(TDataModule) private { Private declarations } public { Public declarations } function Login(Credentials: TJSONObject): string; end; {$METHODINFO OFF} implementation {$R *.dfm} uses StrUtils; function TServerMethods1.Login(Credentials: TJSONObject): string; begin Result := '{"token": "test123"}'; end; end. |
後端的程式內容非常簡單,cors 則在 webpack.config.js 就搞定,這裡就不再詳述,而 login.js 的 loginUser 函式則作了點小小小的異動:
1 2 3 4 5 6 7 8 9 10 11 12 13 | // ./src/views/login.js async function loginUser(credentials){ return fetch('/datasnap/rest/TServerMethods1/"Login"',{ method:'POST', headers:{ 'Content-Type': 'application/json' }, body: JSON.stringify(credentials) }) .then(response=>response.json()) .then(resultObj=>resultObj[0]) } |
完成後你將可以看到登入畫面以及問卷內容和成績統計:
之後呢?
哈哈,被發現了嗎,本次範例雖然將問卷全流程解析,但並沒有後端計算成績的 API,但對你來說,到了這個階段都是分秒能完成的小事了,留待你的各種發揮 😉
總結
在看完 Joe Morgan 所寫的【How To Add Login Authentication to React Applications】後,真的很開心自己也能透過作者的文章做出更加延伸的應用程式,趁著記憶猶新,趕緊記錄下來。
本次的程式碼會放在 See also 內。
和你分享 😉
沒有留言:
張貼留言