Form

Form in React

2021/07/08 (改寫內容)
2021/10/
13 (更新內容)

Form

在PHP(或傳統的web)裡,是透用Form以及http post,在submit後,送出Form裡的欄位內容給伺服器。在javascript裡,可以利用document.getElementById取得DOM的內容,再利用http post將資料送到伺服器。在介紹Firebase時,我們會介紹如何將資料送到伺服器。

在react裡,並不建議直接去取得DOM的內容,所以,寫法就跟javascript不一樣了。在react裡,則是在輸入時(onChange)就將內容放到state裡,並將value設為state。(詳參: Handling EventsForms)

產品描述:<input type="text" name="desc" value={desc} onChange={handleClickDesc}/><br/>

產品價格:<input type="number" name="price" value={price} onChange={handleClickPrice}/><br/>

e是onChange預設傳給被呼叫方法的第一個參數,可以改為任何的變數名稱。可以利用console.log(e.target),就可以看到是取得DOM的元件,再從e.target中取得value,將value放到state裡。

handleClickDesc及handleClickPrice

function handleClickDesc(e) {

setDesc(e.target.value);

}

function handleClickPrice(e) {

setPrice(e.target.value);

}

在點擊按鈕時,就可以呼叫update函數。

<button onClick={update}>新增</button>

update函數可以傳兩個變數

const update = function(){

props.update(desc,price);

}

也可以將兩個變數包成一個物件

const update = function(){

props.update({desc,price});

}

包成物件的好處是如果增加第三個變數,接收端不用改參數。當我們程式很龐大,ProductAdd被多個元件使用是,這樣的作法,可以讓原有的元件還能使用,需要第三個變數的元件就在接收端加上變數即可。當然,也有人認為如果新增變數就應該改參數,否則會有意外。所以,就看使用的情境決定如何傳變數。

這個寫法,其實就是,

const update = function(){

props.update({desc:desc,price:price});

}

當物件屬性與對應變數名稱一樣,就可以省略。

update函數則呼叫ProductList裡的update函數。這樣的做法稱為callback method,如果只是簡單的父對子,可以使用這樣的方式,但是,如果元件的關係很多很複雜,這樣的做法就會產生所謂的call back hell,解決的方法有:context或者使用redux(或類似的方法)。

也可以將兩個useState合併

const [product, setProduct] = useState({desc:"",price:0})

handleClickDesc的部分要改成,也就是覆蓋掉desc的內容:

const handleClickDesc = function(e) {

setProduct({...product,desc:e.target.value});

}

update的部分,改成:

const update = function(){

props.update(product);

}

再把兩個handleClick整合起來,取得欄位的名稱:

const handleClick = function(e){

setProduct({...product,[e.target.name]:e.target.value})

}

完整程式

src/product/ProductAdd.js

import React, {useState} from 'react';

export default function ProductAdd(props) {

const [product, setProduct] = useState({desc:"",price:0})


const handleClick = function(e){

setProduct({...product,[e.target.name]:e.target.value})

}


const update = function(){

props.update(product);

}


return (

<div>

產品描述:<input type="text" name="desc" value={product.desc} onChange={handleClick}/><br/>

產品價格:<input type="number" name="price" value={product.price} onChange={handleClick}/><br/>

<button onClick={update}>新增</button>

</div>

);

}

在ProductList.js裡要有update函數來接收ProductAdd傳來的變數

setState (setProducts)接受兩種參數,第一種參數是個值,setState會更新state的內容,但是如果要利用原本的值,就會有問題。

所以,必須用第二種傳遞方式,就是接受一個callback函數,當參數是callback函數時,setState會將原有的值以及props傳給callback函數。 (詳參:State 和生命週期)

在下面範例裡,我們只使用原有的值,將這個值命名為oldProduct。在ProductList中,將ProductAdd傳回的newProduct,利用setProducts利用spread將收到的內容加到陣列最後。

spread語法請詳參: Day 09: ES6篇: Spread Operator & Rest Operator(展開與其餘運算符)

const insert = function (newProduct){

setProducts(oldProducts=>[...oldProducts, newProduct])

}

** 注意,這裡不能用push ,下面的語法是無法有效更新products **

直接push無法更動內容

const insert = function(newProduct){

products.push(newProduct);//not working....

}

也不能這樣更動內容

const insert = function(newProduct){

products.push(newProduct);

setProducts(products);//not working....

}

再把剛剛的ProductAdd加進來

<ProductAdd update={update}/>

src/product/ProductList.js

import ProductAdd from './ProductAdd'

import React, {useState} from 'react';

export default function ProductList() {

const [products, setProducts] = useState([

{desc:"iPad", price:20000},

{desc:"iPhone X", price:30000}

]);



const insert = function(newProduct){

setProducts(oldProducts=>[...oldProducts, newProduct]);

}


return (

<div>

<ul>

{products.map((product, index) => <li key={index}>{index} / {product.desc} / {product.price}</li>)}

</ul>

<ProductAdd update={insert}/>

</div>

);

}

Material-UI

Material-UI裡有很多元件可使用,例如,我們可以在ProductAdd裡使用DialogInputTextFieldButton

Input

使用Input 的話,type可以使用html5的有效type (詳參: <input>: The Input (Form Input) element ),其他的props其實跟html是幾乎一樣的,唯一不同的是要用onChange,而不是onchange。

產品描述:<Input type="text" name="desc" value={desc} onChange={handleClickDesc}/><br/>

TextField

TextField 是由很多的元件組合可以設定不同的樣式,會更像手機的介面。

<TextField label ="產品描述" variant="outlined" value={desc} onChange={handleClickDesc}/>

Button

使用Button時,可利用props設定樣式及顏色。

<Button variant="contained" color="primary" onClick={update}>新增</Button>

在ProductList裡可以使用ListsFloating action button 。如果上週的作業還沒完成的,就請利用這機會完成。

Dialog

可以利用Dialog,來顯示內容,將Input及Button放在Dialog裡。

<Dialog>

產品描述:<Input type="text" name="desc" value={product.desc} onChange={handleClick}/><br/>

產品價格:<Input type="number" name="price" value={product.price} onChange={handleClick}/><br/>

<Button variant="contained" color="primary" onClick={update}>新增</Button>

</Dialog>

Dialog有一些props可以使用,open如果是true,就顯示dialog,如果是flase,就不顯示dialog。onClose則是處理關閉dialog的動作,在這裡可以把open設為false。

另外,可利用DialogTitle APIDialogContent APIDialogContentText APIDialogActions API 來配置內容的位置。

<Dialog open={props.open} onClose={handleClose} aria-labelledby="新增產品">

<DialogTitle>"新增產品"</DialogTitle>


</Dialog>

Floating Action Button / FAB

可以利用Floating action button來啟動ProductAdd,想想看要怎麼改,才能顯示及不顯示dialog?

<Fab color="primary" aria-label="Add">

+

</Fab>

作業

  • 將上面的範例套用Material-UI。

進階挑戰

  • 如何刪除內容?

    • 在ListItem裡增加ICON,讓使用者點選ICON就可以刪除內容

<IconButton edge="end" aria-label="delete">

<DeleteIcon />

</IconButton>

要安裝@mui/icons-material

npm install @mui/icons-material

要import

import DeleteIcon from '@mui/icons-material/Delete';

或者

import {Delete as DeleteIcon} from '@mui/icons-material';

    • 在javascript裡,如何刪除陣列的內容? ** 記得,不能直接操作state變數! **

temp.splice(index,1);

  • 如何修改內容?

    • 在ListItem裡增加ICON,讓使用者點選ICON就可以修改內容

<IconButton edge="end" aria-label="edit">

<EditIcon />

</IconButton>

    • 將點選內容放到Dialog裡,讓使用者可以修改,可以利用ProductAdd,改成也可以修改,也可以兩個不同的元件

    • 在javascript裡,如何修改陣列的內容? ** 記得,不能直接操作state變數! **

參考資料