Thử thách: Xây dựng giao diện dữ liệu nhà
Trong thử thách này, chúng ta sẽ yêu cầu bạn viết một số JavaScript cho trang tìm kiếm/lọc nhà trên trang web bất động sản. Điều này sẽ bao gồm tải dữ liệu JSON, lọc dữ liệu đó dựa trên các giá trị được nhập vào các điều khiển biểu mẫu được cung cấp và hiển thị dữ liệu đó lên giao diện. Trong quá trình thực hiện, chúng ta cũng sẽ kiểm tra kiến thức của bạn về câu lệnh điều kiện, vòng lặp, mảng và các phương thức mảng, v.v.
Điểm khởi đầu
Để bắt đầu, hãy nhấp nút Play trong một trong các panel mã dưới đây để mở ví dụ được cung cấp trong MDN Playground. Sau đó bạn sẽ làm theo các hướng dẫn trong phần Tóm tắt dự án để hoàn thành chức năng JavaScript.
<h1>House search</h1>
<p>
Search for houses for sale. You can filter your search by street, number of
bedrooms, and number of bathrooms, or just submit the search with no filters
to display all available properties.
</p>
<form>
<div>
<label for="choose-street">Street:</label>
<select id="choose-street" name="choose-street">
<option value="">No street selected</option>
</select>
</div>
<div>
<label for="choose-bedrooms">Number of bedrooms:</label>
<select id="choose-bedrooms" name="choose-bedrooms">
<option value="">Any number of bedrooms</option>
</select>
</div>
<div>
<label for="choose-bathrooms">Number of bathrooms:</label>
<select id="choose-bathrooms" name="choose-bathrooms">
<option value="">Any number of bathrooms</option>
</select>
</div>
<div>
<button>Search for houses</button>
</div>
</form>
<p id="result-count">Results returned: 0</p>
<section id="output"></section>
const streetSelect = document.getElementById("choose-street");
const bedroomSelect = document.getElementById("choose-bedrooms");
const bathroomSelect = document.getElementById("choose-bathrooms");
const form = document.querySelector("form");
const resultCount = document.getElementById("result-count");
const output = document.getElementById("output");
let houses;
function initializeForm() {
}
function renderHouses(e) {
// Stop the form submitting
e.preventDefault();
// Add rest of code here
}
// Add a submit listener to the <form> element
form.addEventListener("submit", renderHouses);
// Call fetchHouseData() to initialize the app
fetchHouseData();
Tóm tắt dự án
Bạn được cung cấp trang HTML index chứa biểu mẫu cho phép người dùng tìm kiếm nhà theo phố, số phòng ngủ và số phòng tắm, cộng với một vài phần tử để chứa kết quả tìm kiếm. Bạn cũng được cung cấp tệp JavaScript chứa một số định nghĩa hằng số và biến, cộng với một vài định nghĩa hàm khung. Công việc của bạn là điền vào JavaScript còn thiếu để làm cho giao diện tìm kiếm nhà hoạt động.
Các định nghĩa hằng số và biến được cung cấp giữ các tham chiếu sau:
streetSelect: Phần tử<select>"choose-street".bedroomSelect: Phần tử<select>"choose-bedrooms".bathroomSelect: Phần tử<select>"choose-bathrooms".form: Phần tử<form>tổng thể chứa các phần tử<select>.resultCount: Phần tử<p>"result-count", cập nhật để hiển thị số kết quả được trả về sau mỗi lần tìm kiếm.output: Phần tử<section>"output", hiển thị kết quả tìm kiếm.houses: Ban đầu rỗng, nhưng điều này sẽ chứa đối tượng dữ liệu nhà được tạo bằng cách phân tích dữ liệu JSON đã tải về.
Các hàm khung là:
initializeForm(): Điều này sẽ truy vấn dữ liệu và điền vào các phần tử<select>với các giá trị có thể được tìm kiếm.renderHouses(): Điều này sẽ lọc dữ liệu dựa trên các giá trị phần tử<select>và hiển thị kết quả.
Tải dữ liệu
Điều đầu tiên bạn cần làm là tạo hàm mới để tải dữ liệu nhà và lưu vào biến houses.
Để làm điều đó:
- Tạo hàm mới ngay bên dưới các định nghĩa biến và hằng số gọi là
fetchHouseData(). - Bên trong thân hàm, sử dụng phương thức
fetch()để tải JSON tìm thấy tại https://mdn.github.io/shared-assets/misc/houses.json. Bạn nên nghiên cứu cấu trúc của dữ liệu này để chuẩn bị cho một số bước sau. - Khi promise kết quả giải quyết, kiểm tra thuộc tính
okcủa phản hồi. Nếu nó làfalse, ném lỗi tùy chỉnh báo cáostatuscủa phản hồi. - Với điều kiện phản hồi ổn, trả về phản hồi dưới dạng JSON bằng phương thức
json(). - Khi promise kết quả giải quyết, đặt biến
housesbằng kết quả của phương thứcjson()(đây nên là mảng các đối tượng chứa dữ liệu nhà), và gọi hàminitializeForm().
Hoàn thành hàm initializeForm()
Bây giờ bạn cần viết nội dung của hàm initializeForm(). Điều này sẽ truy vấn dữ liệu được lưu bên trong houses và sử dụng nó để điền vào các phần tử <select> với các phần tử <option> đại diện cho tất cả các giá trị khác nhau có thể được lọc. Hiện tại, các phần tử <select> chỉ chứa một phần tử <option> duy nhất với giá trị "" (chuỗi rỗng), đại diện cho tất cả các giá trị. Người dùng có thể chọn tùy chọn này nếu họ không muốn kết quả được lọc theo trường đó.
Bên trong thân hàm, viết mã thực hiện như sau:
- Tạo các phần tử
<option>cho tất cả các tên phố khác nhau bên trong<select>"choose-street". Có một vài cách bạn có thể làm điều này, nhưng chúng tôi khuyên bạn tạo mảng tạm thời sau đó lặp qua tất cả các đối tượng bên tronghouses. Bên trong vòng lặp, kiểm tra xem mảng tạm thời có bao gồm thuộc tínhstreetcủa nhà hiện tại không. Nếu không, thêm nó vào mảng tạm thời và thêm<option>vào<select>"choose-street" bao gồm thuộc tínhstreetlàm giá trị của nó. - Tạo các tùy chọn cho tất cả các giá trị số phòng ngủ có thể bên trong
<select>"choose-bedrooms". Để làm điều này, bạn có thể lặp qua mảnghousesvà xác định giá trịbedroomslớn nhất là gì, sau đó viết vòng lặp thứ hai thêm<option>vào<select>"choose-bedrooms" cho mỗi số từ1đến giá trị lớn nhất đó. - Tạo các tùy chọn cho tất cả các giá trị số phòng tắm có thể bên trong
<select>"choose-bathrooms". Điều này có thể được giải quyết bằng kỹ thuật tương tự như bước trước.
Note:
Bạn có thể chỉ cần mã hóa cứng các phần tử <option> bên trong HTML, nhưng điều đó chỉ hoạt động cho tập dữ liệu chính xác này. Chúng ta muốn bạn viết JavaScript sẽ điền chính xác biểu mẫu bất kể các giá trị dữ liệu được cung cấp (mỗi đối tượng nhà phải có cùng cấu trúc).
Note:
Bạn có thể sử dụng thuộc tính innerHTML để thêm nội dung con bên trong các phần tử HTML, nhưng chúng tôi khuyên bạn không làm như vậy. Bạn không thể luôn tin tưởng dữ liệu bạn đang thêm vào trang của mình: Nếu không được làm sạch đúng cách trên máy chủ, những kẻ xấu có thể sử dụng innerHTML như một con đường để thực hiện các cuộc tấn công Cross-site scripting (XSS) trên trang của bạn. Con đường an toàn hơn là sử dụng các tính năng DOM scripting như createElement(), appendChild() và textContent. Sử dụng innerHTML để xóa nội dung con không phải là vấn đề như vậy.
Hoàn thành hàm renderHouses()
Tiếp theo, bạn cần hoàn thành thân hàm renderHouses(). Điều này sẽ lọc dữ liệu dựa trên các giá trị phần tử <select> và hiển thị kết quả lên giao diện.
- Trước tiên, bạn cần lọc dữ liệu. Điều này có thể đạt được tốt nhất bằng cách sử dụng phương thức
filter()của mảng, trả về mảng mới chỉ chứa các phần tử mảng khớp với tiêu chí lọc.- Đây là hàm
filter()khá phức tạp để viết. Bạn cần kiểm tra xem thuộc tínhstreetcủa nhà có bằng giá trị được chọn của<select>"choose-street", và thuộc tínhbedroomscủa nhà có bằng giá trị được chọn của<select>"choose-bedrooms", và thuộc tínhbathroomscủa nhà có bằng giá trị được chọn của<select>"choose-bathrooms". - Mỗi thành phần của kiểm tra luôn cần trả về
truenếu giá trị<select>liên kết là""(chuỗi rỗng, đại diện cho tất cả các giá trị). Bạn có thể đạt được điều này bằng cách "ngắn mạch" mỗi lần kiểm tra. - Bạn cũng cần đảm bảo các kiểu dữ liệu khớp trong mỗi lần kiểm tra. Giá trị của phần tử biểu mẫu luôn là chuỗi. Đây không nhất thiết là trường hợp cho các giá trị thuộc tính đối tượng của bạn. Làm thế nào để các kiểu dữ liệu khớp cho mục đích kiểm tra?
- Đây là hàm
- Xuất số kết quả tìm kiếm được lọc vào phần tử
<p>"result-count", sử dụng cấu trúc chuỗi "Results returned: number". - Làm trống phần tử
<section>"output", vì vậy nó không có bất kỳ phần tử HTML con nào. Nếu bạn không làm điều này, mỗi khi tìm kiếm được thực hiện kết quả sẽ được thêm vào cuối kết quả trước đó thay vì thay thế chúng. - Tạo hàm mới bên trong
renderHouses()gọi làrenderHouse(). Hàm này cần lấy đối tượng nhà làm đối số và thực hiện hai điều:- Tính tổng diện tích của các phòng bên trong đối tượng
room_sizescủa nhà. Điều này không đơn giản như lặp qua mảng số và cộng tổng chúng, nhưng không quá khó. - Thêm phần tử
<article>bên trong phần tử<section>"output" chứa số nhà, tên phố, số phòng ngủ và phòng tắm, tổng diện tích phòng và giá. Bạn có thể thay đổi cấu trúc nếu muốn, chúng tôi muốn nó tương tự như đoạn HTML này:
html<article> <h2>number street name</h2> <ul> <li>🛏️ Bedrooms: number</li> <li>🛀 Bathrooms: number</li> <li>Room area: number m²</li> <li>Price: £price</li> </ul> </article> - Tính tổng diện tích của các phòng bên trong đối tượng
- Lặp qua tất cả các nhà bên trong mảng đã lọc và truyền mỗi cái vào lời gọi
renderHouse().
Gợi ý và mẹo
- Bạn không cần thay đổi HTML hoặc CSS theo bất kỳ cách nào.
- Để làm những việc như tìm giá trị lớn nhất trong mảng giá trị, hàm mảng
reduce()thực sự tiện dụng. Chúng ta chưa dạy nó trong khóa học này, vì nó khá phức tạp, nhưng nó thực sự mạnh mẽ khi bạn hiểu được nó. Như mục tiêu mở rộng, hãy thử nghiên cứu và sử dụng nó trong câu trả lời của bạn.
Ví dụ
Ứng dụng hoàn chỉnh của bạn sẽ hoạt động như ví dụ trực tiếp sau:
Nhấp vào đây để hiển thị giải pháp
JavaScript hoàn chỉnh sẽ trông giống như thế này:
const streetSelect = document.getElementById("choose-street");
const bedroomSelect = document.getElementById("choose-bedrooms");
const bathroomSelect = document.getElementById("choose-bathrooms");
const form = document.querySelector("form");
const resultCount = document.getElementById("result-count");
const output = document.getElementById("output");
let houses;
// Solution: Fetching the data
function fetchHouseData() {
fetch("https://mdn.github.io/shared-assets/misc/houses.json")
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then((json) => {
houses = json;
initializeForm();
});
}
// Solution: Completing the initializeForm() function
function initializeForm() {
// Create options for all the different street names
const streetArray = [];
for (let house of houses) {
if (!streetArray.includes(house.street)) {
streetArray.push(house.street);
streetSelect.appendChild(document.createElement("option")).textContent =
house.street;
}
}
// Create options for all the possible bedroom values
const largestBedrooms = houses.reduce(
(largest, house) => (house.bedrooms > largest ? house.bedrooms : largest),
houses[0].bedrooms,
);
let i = 1;
while (i <= largestBedrooms) {
bedroomSelect.appendChild(document.createElement("option")).textContent = i;
i++;
}
// Create options for all the possible bathroom values
const largestBathrooms = houses.reduce(
(largest, house) => (house.bathrooms > largest ? house.bathrooms : largest),
houses[0].bathrooms,
);
let j = 1;
while (j <= largestBathrooms) {
bathroomSelect.appendChild(document.createElement("option")).textContent =
j;
j++;
}
}
// Solution: Completing the renderHouses() function
function renderHouses(e) {
// Stop the form submitting
e.preventDefault();
// Filter the data
const filteredHouses = houses.filter((house) => {
// prettier-ignore
const test = (streetSelect.value === "" ||
house.street === streetSelect.value) &&
(bedroomSelect.value === "" ||
String(house.bedrooms) === bedroomSelect.value) &&
(bathroomSelect.value === "" ||
String(house.bathrooms) === bathroomSelect.value);
return test;
});
// Output the result count to the "result-count" paragraph
resultCount.textContent = `Results returned: ${filteredHouses.length}`;
// Empty the output element
output.innerHTML = "";
// Create renderHouse() function
function renderHouse(house) {
// Calculate total room size
let totalArea = 0;
const keys = Object.keys(house.room_sizes);
for (let key of keys) {
totalArea += house.room_sizes[key];
}
// Output house to UI
const articleElem = document.createElement("article");
articleElem.appendChild(document.createElement("h2")).textContent =
`${house.house_number} ${house.street}`;
const listElem = document.createElement("ul");
listElem.appendChild(document.createElement("li")).textContent =
`🛏️ Bedrooms: ${house.bedrooms}`;
listElem.appendChild(document.createElement("li")).textContent =
`🛀 Bathrooms: ${house.bathrooms}`;
listElem.appendChild(document.createElement("li")).textContent =
`Room area: ${totalArea}m²`;
listElem.appendChild(document.createElement("li")).textContent =
`Price: £${house.price}`;
articleElem.appendChild(listElem);
output.appendChild(articleElem);
}
// Pass each house in the filtered array into renderHouse()
for (let house of filteredHouses) {
renderHouse(house);
}
}
// Add a submit listener to the <form> element
form.addEventListener("submit", renderHouses);
// Call fetchHouseData() to initialize the app
fetchHouseData();