最後來打造「依測站名稱查詢」的功能。
關鍵字打上React Leaflet Search,有些外掛看起來是包含世界各地的經緯數據。比較廣泛,也不見得會有測站數據。
抓input裡的值去改StationName應該比較有效。
一不做二不休,但直接在index寫這樣不行。
<div id="root">
<div id="search">
<label>
鄉鎮別:
<input placeholder="台北市大安區 or 大安區" />
<button>查詢</button>
</label>
</div>
</div>
那麼就再開一個Search組件。
熟悉的紅字最對味:Cannot read properties of null (reading 'value')。
直覺是先寫useEffect試試。
不過此時還須搭配useRef和useChange才行。
解決完null值問題,再讓input在發生變化時也可以取得最新資訊。
import { useState, useEffect, useRef } from "react";
export default function Search() {
const [iv, setIv] = useState("");
const ir = useRef(null);
useEffect(() => {
if (ir.current) {
setIv(ir.current.value);
}
}, []);
const handleChange = (e) => {
setIv(e.target.value);
};
const handleSearch = () => {
console.log(iv);
};
return (
<div className="search">
<label>
站名:
<input
ref={ir}
placeholder="e.g. 阿里山國小"
onChange={handleChange}
></input>
<button onClick={handleSearch}>查詢</button>
</label>
</div>
);
}
接下來用傳遞props的概念,跨JS檔傳遞iv (input value)值。
再稍微整理一下程式碼之後變成這樣:
//Search.js
import "./styles.css";
import "leaflet/dist/leaflet.css";
import { useState, useEffect, useRef } from "react";
export default function Search({ onSearch }) {
const [iv, setIv] = useState("");
const ir = useRef(null);
useEffect(() => {
if (ir.current) {
setIv(ir.current.value);
}
}, []);
const handleChange = (e) => {
setIv(e.target.value);
};
const handleSearch = () => {
onSearch(iv);
};
return (
<div className="search">
<label>
站名:
<input
ref={ir}
placeholder="e.g. 阿里山國小"
onChange={handleChange}
></input>
<button onClick={handleSearch}>查詢</button>
</label>
</div>
);
}
//App.js
import "./styles.css";
import "leaflet/dist/leaflet.css";
import {
MapContainer,
Marker,
Popup,
SVGOverlay,
TileLayer,
useMap,
} from "react-leaflet";
import { useState, useEffect } from "react";
import Search from "./Search";
import L from "leaflet";
L.Icon.Default.imagePath = "https://unpkg.com/leaflet/dist/images/";
function ChangeView({ position }) {
const map = useMap();
useEffect(() => {
map.setView(position);
}, [position, map]);
return null;
}
export default function App() {
const [name, setName] = useState("");
const [rain, setRain] = useState(0);
const [la, setLa] = useState(0);
const [lo, setLo] = useState(0);
const [bounds, setBounds] = useState([
[0, 0],
[0, 0],
]);
const [searchName, setSearchName] = useState("嘉義");
const position = [la, lo];
const handleSearch = (value) => {
setSearchName(value);
};
useEffect(() => {
fetch(
"https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
)
.then((res) => res.json())
.then(
(resJson) =>
resJson.records.Station.filter((s) => s.StationName === searchName)[0]
.StationName
)
.then((resJson) => setName(resJson))
.catch((err) => console.log(err));
}, [searchName]);
useEffect(() => {
fetch(
"https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
)
.then((res) => res.json())
.then(
(resJson) =>
resJson.records.Station.filter((s) => s.StationName === searchName)[0]
.RainfallElement.Now.Precipitation
)
.then((resJson) => setRain(resJson))
.catch((err) => console.log(err));
}, [searchName]);
useEffect(() => {
fetch(
"https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
)
.then((res) => res.json())
.then(
(resJson) =>
resJson.records.Station.filter((s) => s.StationName === searchName)[0]
.GeoInfo.Coordinates[1].StationLatitude
)
.then((resJson) => setLa(resJson))
.catch((err) => console.log(err));
}, [searchName]);
useEffect(() => {
fetch(
"https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
)
.then((res) => res.json())
.then(
(resJson) =>
resJson.records.Station.filter((s) => s.StationName === searchName)[0]
.GeoInfo.Coordinates[1].StationLongitude
)
.then((resJson) => setLo(resJson))
.catch((err) => console.log(err));
}, [searchName]);
useEffect(() => {
fetch(
"https://opendata.cwa.gov.tw/api/v1/rest/datastore/O-A0002-001?Authorization=你的API key"
)
.then((res) => res.json())
.then((resJson) => {
const station = resJson.records.Station.find(
(s) => s.StationName === searchName
);
const la = station.GeoInfo.Coordinates[1].StationLatitude;
const lo = station.GeoInfo.Coordinates[1].StationLongitude;
setBounds([
[la, lo - 0.07],
[la + 0.07, lo + 0.05],
]);
})
.catch((err) => console.log(err));
}, [searchName]);
return (
<>
<Search onSearch={handleSearch} />
<MapContainer center={position} zoom={12} scrollWheelZoom={true}>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"
/>
<Marker position={position}>
<Popup>
{name}, {rain}mm
</Popup>
</Marker>
<ChangeView position={position} />
{rain > 0 && (
<SVGOverlay key={JSON.stringify(bounds)} bounds={bounds}>
<defs>
<symbol id="drop" viewBox="0 -10 80 10">
<line stroke="#4ea6e9" strokeWidth="1%">
<animate
attributeName="x1"
from="30"
to="0"
dur="1s"
repeatCount="indefinite"
/>
<animate
attributeName="y1"
from="0"
to="60"
dur="1s"
repeatCount="indefinite"
/>
<animate
attributeName="x2"
from="30"
to="15"
dur="1s"
repeatCount="indefinite"
/>
<animate
attributeName="y2"
from="0"
to="30"
dur="1s"
repeatCount="indefinite"
/>
</line>
</symbol>
</defs>
<use xlinkHref="#drop" x="0" y="0" />
<use xlinkHref="#drop" x="10%" y="0" />
<use xlinkHref="#drop" x="20%" y="0" />
<use xlinkHref="#drop" x="30%" y="0" />
<use xlinkHref="#drop" x="40%" y="0" />
<use xlinkHref="#drop" x="50%" y="0" />
<use xlinkHref="#drop" x="60%" y="0" />
<use xlinkHref="#drop" x="70%" y="0" />
<use xlinkHref="#drop" x="80%" y="0" />
<use xlinkHref="#drop" x="90%" y="0" />
</SVGOverlay>
)}
</MapContainer>
</>
);
}
我們首先將iv包在onSearch()裡,然後放到Search的括號中,就像props。
這樣當使用者在App.js用到Search組件,就可以把躲在onSearch裡的iv值拿來用了。
另一邊,在APP.js裡設定searchName的state,記得不要粗心把初始值一併改成searchName,才不會報錯:Cannot access 'searchName' before initialization。
然後在每個useEffect的依賴項裡填入[searchName]。這樣每當使用者按下查詢引發handleSearch,導致searchName被重新set後,就會從API抓新的值。
並且把<Search>
改成<Search onSearch={handleSearch} />
,才能真正叫得動handleSearch。
測試時發現有個小缺陷是:如果兩間測站太近,地標icon會比圖資更迫不及待地跑走,哈。
最後的最後,我希望能為欄位加入「搜尋建議」功能。
而react-search-autocomplete正好能滿足所求。
基本上就是把所有測站名稱重新mapping成一個陣列。
在這個陣列裡有很多items。當使用者在選中搜尋建議時,則會重新得到items裡的iv (input value)值,也就是items裡被選中的那個測站名稱。再用onSearch和父組件App.js聯繫。
一開始搜尋框長得很怪,把原本search的CSS改掉就好了。
然而明明能正常傳出數值的查詢按鈕卻無法作用。
反倒是框內輸入正確時,只要按enter,視圖就會跳轉。
翻找一下文件,看來這個外掛本來就打算全包搜尋工作。
索性讓它克盡己職。整體效果也十分流暢,很滿意。
再把之前沒特別調的margin和padding歸零,實現真正的全螢幕後——
成功建好一張能搜尋某測站有沒有下雨的漂亮地圖,就是完賽的感人時刻了。
感謝終於沒有輕易言棄的自己。
也感謝點進來看我絮絮叨叨的你。
四年前剛認識開發人員工具。
四年後,若能再和旁人閒聊前端技藝,願我的眼神能更堅定。