#概要
**以前作成したアプリ**に検索フォームを実装しました。
**vte.cxについて詳しく知りたい方はvte.cxのドキュメント**をご覧ください。
今回実装した検索フォームの仕様は以下になります。
- 検索した結果を一覧に表示する。
- 検索した結果でページネーションを行う。
- 入力した値を全文検索するフォームを作る
- 複数(最大すべて)の項目を入力し検索できるフォームを作る
#実装
今回編集したファイルは一覧画面(ListProf)と新しく作った検索フォームファイル(SearchList)になります。
まずは検索フォームファイルの方から見ていきます。
##検索フォーム(SearchList)
import * as React from 'react'
import { useState } from 'react'
type SearchProps = Partial<{
getFeedLength: any,
putIndex: any,
getPage: any
}>
const SearchList:React.FC<SearchProps> = ({getFeedLength, putIndex, getPage}) => {
const [searchText, setSearchText] = useState({})
const [searchFullText, setSearchFullText] = useState('')
const searchFeed = () => {
getFeedLength(searchText, searchFullText)
putIndex(1,searchText, searchFullText)
getPage(1,0, searchText, searchFullText)
}
return (
<form>
<input type="text" placeholder="全文検索" value={searchFullText} onChange={(e:any) => setSearchFullText(e.target.value)} />
<button type="button" onClick={() => searchFeed()}>検索</button><br />
<input type="text" placeholder="名前" onChange={(e:any) => setSearchText({...searchText, name:e.target.value})} />
<input type="text" placeholder="メール" onChange={(e:any) => setSearchText({...searchText, email:e.target.value})} />
<input type="text" placeholder="職業" onChange={(e:any) => setSearchText({...searchText, job:e.target.value})} />
<input type="text" placeholder="住所" onChange={(e:any) => setSearchText({...searchText, address:e.target.value})} />
<input type="text" placeholder="身長" onChange={(e:any) => setSearchText({...searchText, height:e.target.value})} /><br />
<label htmlFor="calendar">生年月日:</label>
<input type="date" onChange={(e:any) => setSearchText({...searchText, birthday:e.target.value})} />
<input type="radio" name="check" onChange={(e:any) => setSearchText({...searchText, gender:e.target.value})} /><label htmlFor="gender">男</label>
<input type="radio" name="check" onChange={(e:any) => setSearchText({...searchText, gender:e.target.value})} /><label htmlFor="gender">女</label>
<input type="checkbox" name='チェックボックス' onChange={() => setSearchText({...searchText, check: 'チェック1'})} checked={Object.values(searchText).indexOf('チェック1') !== -1 && true} />チェック1
<input type="checkbox" name='チェックボックス' onChange={() => setSearchText({...searchText, check: 'チェック2'})} checked={Object.values(searchText).indexOf('チェック2') !== -1 && true}/>チェック2
<input type="checkbox" name='チェックボックス' onChange={() => setSearchText({...searchText, check: 'チェック3'})} checked={Object.values(searchText).indexOf('チェック3') !== -1 && true}/>チェック3<br />
<select onChange={(e:any) => setSearchText({...searchText, select:e.target.value})}>
<option disabled selected value="">選択してください</option>
<option value="サンプル1">サンプル1</option>
<option value="サンプル2">サンプル2</option>
<option value="サンプル3">サンプル3</option>
</select><br />
<textarea placeholder="メモ" onChange={(e:any) => setSearchText({...searchText, memo:e.target.value})}></textarea><br />
<button type="button" onClick={() => searchFeed()}>検索</button>
</form>
)
}
export default SearchList
このSearchListコンポーネントでは、検索フォームに入力された値をsearchText
かsearchFullText
に格納して、検索ボタンが押されたら、searchFeed
関数を実行し画面描画する。という処理を行っています。複数(最大すべて)の項目を検索できるフォームは,スキーマ名と入力された値を格納したいので、オブジェクトにkey:value
の形で格納するようにしました。
次にsearchFeed
関数内で実行している関数を見ていきます。
##一覧画面(ListProf)
import * as React from 'react'
import { useState, useRef } from 'react'
import axios from 'axios'
import { withRouter } from 'react-router-dom'
import Pagination from './Pagination'
import SearchList from './SearchList'
//一覧画面
const ListProf:React.FC = (props:any) => {
const [deletedPage, setDeletedPage] = useState(1)
const [feed, setFeed] = useState<VtecxApp.Entry[]>([])
const [firstIndexPage, setFirstIndexPage] = useState(1)
const [lastIndexPage, setLastIndexPage] = useState(50)
const [fullText, setFullText] = useState('')
const [text, setText] = useState({})
const feedLength = useRef(0)
const lastPage = useRef(0)
const search_url = useRef('')
const getFeedLength = async(searchText:{}, searchFullText:string) => {
try {
search_url.current = '&|user.name-rg-.*'+searchFullText+'.*'
+'&|user.email-rg-.*'+searchFullText+'.*&|user.gender-rg-.*'+searchFullText+'.*'
+'&|user.memo-rg-.*'+searchFullText+'.*&|user.birthday-rg-.*'+searchFullText+'.*'
+'&|user.check-rg-.*'+searchFullText+'.*&|user.select-rg-.*'+searchFullText+'.*'
+'&|user.job-rg-.*'+searchFullText+'.*&|user.address-rg-.*'+searchFullText+'.*'
+'&|user.height-rg-.*'+searchFullText+'.*'
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
//データの総件数取得
if(Object.keys(searchText).length === 0 && searchFullText === '') {
const res = await axios.get('/d/foo?f&c&l=*')
feedLength.current = (Number(res.data.feed.title))
lastPage.current = (Math.ceil(res.data.feed.title / 5))
} else if(searchFullText !== '') {
const res = await axios.get('/d/foo?f'+search_url.current+'&c&l=*')
feedLength.current = (Number(res.data.feed.title))
lastPage.current = (Math.ceil(res.data.feed.title / 5))
} else if(Object.keys(searchText).length > 0) {
const mapText = Object.entries(searchText).map(([key, value]) => {
return key+'-rg-.*'+value+'.*'
})
const str = mapText.join('&|')
const res = await axios.get(`/d/foo?f&|user.${str}&c&l=*`)
feedLength.current = (Number(res.data.feed.title))
lastPage.current = (Math.ceil(res.data.feed.title / 5))
}
} catch {
alert('総件数の取得に失敗しました。')
}
}
const putIndex = async(currentPage:number, searchText:{}, searchFullText:string) => {
try {
let firstIndex= firstIndexPage
let lastIndex= lastIndexPage
if(currentPage > firstIndexPage && currentPage > lastIndexPage){
firstIndex = firstIndex + 50
lastIndex = lastIndex + 50
}
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
if(Object.keys(searchText).length === 0 && searchFullText === '') {
await axios.get('/d/foo?f&_pagination='+firstIndex+','+lastIndex+'&l=5')
} else if(searchFullText !== '') {
await axios.get('/d/foo?f&_pagination='+firstIndex+','+lastIndex+search_url.current+'&l=5')
} else if(Object.keys(searchText).length > 0) {
const mapText = Object.entries(searchText).map(([key, value]) => {
return key+'-rg-.*'+value+'.*'
})
const str = mapText.join('&|')
await axios.get('/d/foo?f&|user.'+str+'&_pagination='+firstIndex+','+lastIndex+'&l=5')
}
setFirstIndexPage(firstIndex)
setLastIndexPage(lastIndex)
} catch {
alert('indexが貼れませんでした。')
}
}
const getPage = async(currentPage:number, retry_count:number, searchText:{},searchFullText:string) => {
try {
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
//ページの取得
if(Object.keys(searchText).length === 0 && searchFullText === '') {
const res = await axios.get('/d/foo?f&n='+ currentPage + '&l=5')
setFeed(res.data)
} else if(searchFullText !== '') {
const res = await axios.get('/d/foo?f'+search_url.current+'&n='+ currentPage + '&l=5')
setFeed(res.data)
setFullText(searchFullText)
} else if(Object.keys(searchText).length > 0) {
const mapText = Object.entries(searchText).map(([key, value]) => {
return key+'-rg-.*'+value+'.*'
})
const str = mapText.join('&|')
const res = await axios.get('/d/foo?f&|user.'+ str+'&n='+ currentPage + '&l=5')
setFeed(res.data)
setText(searchText)
}
} catch(e) {
const retry = () => {
retry_count++
const retryIndex = ():void => {
getPage(currentPage, retry_count, searchText,searchFullText)
}
if(retry_count > 9) {
alert('ページが取得できませんでした')
return false
}else{
setTimeout(() => retryIndex(),1000)
}
}
//indexが貼れていなかった場合10回までリトライ
if(e.response.data.feed.title === "Please make a pagination index in advance.") {
retry()
//ページのリクエストが最終番号以降のリクエストが来た時にindexを追加で貼り直す
} else if(e.response.data.feed.title === "Please set a positive number for Page number.") {
putIndex(currentPage, searchText,searchFullText)
retry()
}
}
}
//削除関数
const deleteEntry = async (entry:VtecxApp.Entry, index:number) => {
try {
axios.defaults.headers['X-Requested-With'] = 'XMLHttpRequest'
if(entry && entry.link) {
const key = entry.link[0].___href
await axios.delete('/d'+ key)
getFeedLength(text, fullText)
putIndex(deletedPage,text, fullText)
if(index === 0 && deletedPage !== 1){
getPage(deletedPage -1, 0, text, fullText)
} else {
getPage(deletedPage, 0, text, fullText)
}
}
alert('削除しました')
} catch (e) {
alert('削除できませんでした')
}
}
return (
<div>
{ feed.length > 0 ?
<div>
<SearchList getFeedLength={getFeedLength}
putIndex={putIndex} getPage={getPage} />
<table>
<tr>
<th>名前</th>
<th>メール</th>
<th>職業</th>
<th>住所</th>
<th>身長</th>
<th>誕生日</th>
<th>性別</th>
<th>チェック</th>
<th>セレクト</th>
<th>メモ</th>
</tr>
{feed.map((entry, index) => (
<tr>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.name}, data: entry, title: 'name', type: 'text'})}>
{entry.user!.name}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.email},data: entry, title: 'email', type: 'text'})}>
{entry.user!.email}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.job},data: entry, title:'job', type: 'text'})}>
{entry.user!.job}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.address},data: entry, title: 'address', type: 'text'})}>
{entry.user!.address}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.height},data: entry, title: 'height', type: 'text'})}>
{entry.user!.height}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.birthday},data: entry, title: 'birthday'})}>
{entry.user!.birthday}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.gender},data: entry, title: 'gender'})}>
{entry.user!.gender}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.check},data: entry, title: 'check'})}>
{entry.user!.check}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.select},data: entry, title: 'select'})}>
{entry.user!.select}</a></td>
<td><a onClick={() => props.history.push({pathname: '/EditProf',
state: {text:entry.user!.memo},data: entry, title: 'memo'})}>
{entry.user!.memo}</a></td>
<td><button onClick={() => deleteEntry(entry, index)}>削除</button></td>
</tr>
))}
</table>
</div>
:
<p>登録されていません</p>
}
<Pagination
getPage={(e:number) => getPage(e, 0, text, fullText)}
putIndex={(e:number) => putIndex(e, text,fullText)}
setDeletedPage={setDeletedPage} getFeedLength={getFeedLength}
feedLength={feedLength.current} lastPage={lastPage.current}
lastIndexPage={lastIndexPage} text={text} fullText={fullText} />
<button onClick={() => props.history.push('/RegisterProf')}>新規登録</button>
</div>
)
}
export default withRouter(ListProf)
getFeedLength
,putIndex
,getPage
の中身がだいぶ変わっていると思います。しかし、これはURLの中身が少し変わっているだけで、ページネーションの基本的な考え方は同じです。
下記は全文検索用のURLになります。
search_url.current = '&|user.name-rg-.*'+searchFullText+'.*'
+'&|user.email-rg-.*'+searchFullText+'.*&|user.gender-rg-.*'+searchFullText+'.*'
+'&|user.memo-rg-.*'+searchFullText+'.*&|user.birthday-rg-.*'+searchFullText+'.*'
+'&|user.check-rg-.*'+searchFullText+'.*&|user.select-rg-.*'+searchFullText+'.*'
+'&|user.job-rg-.*'+searchFullText+'.*&|user.address-rg-.*'+searchFullText+'.*'
+'&|user.height-rg-.*'+searchFullText+'.*'
user.name-rg-.*'+searchFullText+'.*'
のところで、指定したスキーマ名から入力された値を検索しています。rg
を指定すると正規表現になるのですが、-rg-.*{テキスト}.*
とすることで、 指定した値が含まれる検索をすることができます。他の検索方法で検索したい場合は以下をご覧ください。
eq : = (等しい)
lt : < (未満)
le : <= (以下)
gt : > (より大きい)
ge : >= (以上)
ne : != (等しくない)
rg : regex (正規表現に合致する)
fm : 前方一致
bm : 後方一致
ft : 全文検索
asc : 昇順ソート
desc : 降順ソート
URLをよく見ると、&
のところが&|
になっていると思います。これはOR検索といって、&|
を使うことでいずれかの検索条件を満たしたものを表示してくれます。
今回の場合だと、全スキーマ名の中から入力された値を検索して、一致したものを表示してくれます。
このURLをsearch_url
に格納して、それぞれの関数内で実行しています。
if(Object.keys(searchText).length === 0 && searchFullText === '') {
const res = await axios.get('/d/foo?f&n='+ currentPage + '&l=5')
setFeed(res.data)
} else if(searchFullText !== '') {
const res = await axios.get('/d/foo?f'+search_url.current+'&n='+ currentPage + '&l=5')
setFeed(res.data)
setFullText(searchFullText)
} else if(Object.keys(searchText).length > 0) {
const mapText = Object.entries(searchText).map(([key, value]) => {
return key+'-rg-.*'+value+'.*'
})
const str = mapText.join('&|')
const res = await axios.get('/d/foo?f&|user.'+ str+'&n='+ currentPage + '&l=5')
setFeed(res.data)
setText(searchText)
}
また、関数内で実行する処理は、検索フォームに入力されていない時の処理、全文検索フォームに入力されていた時の処理、複数の項目を検索するフォームに入力されていた時の処理の3種類あるため、if文で条件分岐させています。
次に複数の項目を検索するフォームに入力されていた時の処理を見ていきます。
} else if(Object.keys(searchText).length > 0) {
const mapText = Object.entries(searchText).map(([key, value]) => {
return key+'-rg-.*'+value+'.*'
})
const str = mapText.join('&|')
const res = await axios.get(`/d/foo?f&|user.${str}&c&l=*`)
searchText
はオブジェクトになっているため、map
でループさせてmapText
に格納しています。ループさせたら、mapText.join
でカンマ(,)で区切られいてる部分を&|
に変えてつなぎ合わせます。
以上が検索フォームの実装になります。
#終わりに
今回はvte.cxを使った検索フォームの実装を行いました。他にも色々な検索方法があるので、状況によって使い分けられるように、また理解を深めていきたいと思います。
また、画面描画用の関数を実行する際に引数(searchText
,searchFullText
)が増えたのを忘れて指定するのを忘れてデバッグに結構時間を使ってしまいました。
join
やmap
なども使ってReactだけではなく、JavaScriptの復習にもなりました。
以上。