挑战:构建房屋数据UI
在此挑战中,我们将让你为一个房地产网站上的房屋搜索/筛选页面编写一些JavaScript。这将包括获取JSON数据,根据提供的表单控件中输入的值筛选数据,并将数据渲染到UI。在此过程中,我们还将测试你对条件语句、循环、数组和数组方法等知识的掌握程度。
起始点
首先,点击下方代码面板中的播放按钮,在MDN Playground中打开提供的示例。然后,你将按照项目简介部分中的说明完成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();
项目简介
我们为你提供了一个HTML索引页面,其中包含一个表单,允许用户按街道、卧室数量和浴室数量搜索房屋,以及几个用于显示搜索结果的元素。我们还为你提供了一个JavaScript文件,其中包含一些常量和变量定义,以及几个骨架函数定义。你的任务是填写缺失的JavaScript,使房屋搜索界面正常工作。
提供的常量和变量定义包含以下引用:
streetSelect
:“choose-street”<select>
元素。bedroomSelect
:“choose-bedrooms”<select>
元素。bathroomSelect
:“choose-bathrooms”<select>
元素。form
:包含<select>
元素的整个<form>
元素。resultCount
:“result-count”<p>
元素,每次搜索后都会更新以显示返回的结果数量。output
:“output”<section>
元素,用于显示搜索结果。houses
:最初为空,但它将包含通过解析获取的JSON数据创建的房屋数据对象。
骨架函数是:
initializeForm()
:这将查询数据并用可能用于搜索的值填充<select>
元素。renderHouses()
:这将根据<select>
元素值筛选数据,并渲染结果。
获取数据
你需要做的第一件事是创建一个新函数来获取房屋数据并将其存储在 houses
变量中。
为此:
- 在变量和常量定义下方创建一个名为
fetchHouseData()
的新函数。 - 在函数体内部,使用
fetch()
方法获取位于 https://mdn.github.io/shared-assets/misc/houses.json 的JSON。你应该研究此数据的结构,以准备后续步骤。 - 当生成的Promise解决时,检查响应的
ok
属性。如果为false
,则抛出一个自定义错误,报告响应的status
。 - 如果响应正常,使用
json()
方法将响应作为JSON返回。 - 当生成的Promise解决时,将
houses
变量设置为json()
方法的结果(这应该是一个包含房屋数据对象的数组),并调用initializeForm()
函数。
完成 initializeForm()
函数
现在你需要编写 initializeForm()
函数的内容。这将查询存储在 houses
中的数据,并使用它填充 <select>
元素,其中包含代表所有可筛选的不同值的 <option>
元素。目前,<select>
元素只包含一个 <option>
元素,其值为 ""
(空字符串),表示所有值。如果用户不想按该字段筛选结果,他们可以选择此选项。
在函数体内部,编写执行以下操作的代码:
- 为“choose-street”
<select>
中所有不同的街道名称创建<option>
元素。有几种方法可以做到这一点,但我们建议创建一个临时数组,然后遍历houses
中的所有对象。在循环内部,检查你的临时数组是否包含当前房屋的street
属性。如果不包含,则将其添加到临时数组,并向“choose-street”<select>
添加一个<option>
,其中包含street
属性作为其值。 - 为“choose-bedrooms”
<select>
中所有可能的卧室数量值创建选项。为此,你可以遍历houses
数组并确定最大的bedrooms
值,然后编写第二个循环,为从1
到最大值的每个数字向“choose-bedrooms”<select>
添加一个<option>
。 - 为“choose-bathrooms”
<select>
中所有可能的浴室数量值创建选项。这可以通过与上一步相同的技术来解决。
注意: 你可以直接在HTML中硬编码 <option>
元素,但这仅适用于此精确数据集。我们希望你编写的JavaScript能够正确填充表单,而无论提供的数据值如何(每个房屋对象都必须具有相同的结构)。
注意: 你可以使用 innerHTML
属性在HTML元素中添加子内容,但我们建议不要这样做。你不能总是信任添加到页面中的数据:如果它在服务器上没有正确清理,恶意攻击者可能会利用 innerHTML
作为途径,在你的页面上执行跨站脚本(XSS)攻击。更安全的方法是使用DOM脚本功能,例如 createElement()
、appendChild()
和 textContent
。使用 innerHTML
删除子内容则不是问题。
完成 renderHouses()
函数
接下来,你需要完成 renderHouses()
函数体。这将根据 <select>
元素值筛选数据,并将结果渲染到UI。
- 首先,你需要筛选数据。这可能最好通过使用数组
filter()
方法来实现,该方法返回一个新数组,其中只包含符合筛选条件的数组元素。- 这是一个相当复杂的
filter()
函数。你需要测试房屋的street
属性是否等于“choose-street”<select>
的选定值,以及房屋的bedrooms
属性是否等于“choose-bedrooms”<select>
的选定值,以及房屋的bathrooms
属性是否等于“choose-bathrooms”<select>
的选定值。 - 如果关联的
<select>
值为""
(空字符串,表示所有值),则测试的每个组件都始终需要返回true
。你可以通过“短路”每个检查来实现这一点。 - 你还需要确保每次检查中的数据类型匹配。表单元素的值始终是字符串。这对于你的对象属性值不一定如此。你如何使数据类型在测试目的上匹配?
- 这是一个相当复杂的
- 使用字符串结构“Results returned: number”将筛选后的搜索结果数量输出到“result-count”
<p>
元素中。 - 清空“output”
<section>
元素,使其不包含任何子HTML元素。如果你不这样做,每次执行搜索时,结果都会添加到之前结果的末尾,而不是替换它们。 - 在
renderHouses()
内部创建一个名为renderHouse()
的新函数。此函数需要将房屋对象作为参数,并执行两件事:- 计算房屋
room_sizes
对象中包含的房间总面积。这不像遍历数字数组并求和那样简单,但也不是太棘手。 - 在“output”
<section>
元素内部添加一个<article>
元素,其中包含房屋的编号、街道名称、卧室和浴室数量、房间总面积和价格。如果你喜欢,可以改变结构,但我们希望它类似于以下HTML代码片段:
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>
- 计算房屋
- 遍历筛选数组中的所有房屋,并将每个房屋传递给
renderHouse()
调用。
提示和技巧
- 你无需以任何方式更改HTML或CSS。
- 对于查找值数组中的最大值等操作,
reduce()
数组函数非常方便。我们没有在本课程中教授它,因为它相当复杂,但当你掌握它时,它非常强大。作为一个延伸目标,尝试研究它并在你的答案中使用它。
示例
你完成的应用程序应该像以下实时示例一样工作:
点击此处显示解决方案
完成的JavaScript应该如下所示:
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();