Before this lesson

Understand array filtering, form events and safe DOM rendering. Complete the course-card layout first.

What you will be able to do

Combine keyword and topic criteria with predictable reset and empty-result behavior.

Understand the mechanism

Define the rules first: case-insensitive title matching, no topic restriction for all, and both conditions must match. Explicit rules prevent UI choices diverging from data behavior. This example uses local data without requests.

Keep the source courses and derive results with filter. Never delete nonmatching entries or progressively mutate the source. Rendering reads controls, replaces old nodes and updates the result count or empty message.

Reset clears query and topic, re-renders and returns focus to search. role=status exposes result changes to assistive technology. Large catalogs need server querying and pagination rather than downloading every record.

Results are plain text because the exercise has no detail pages. Add real URLs and links when connecting actual courses. Validate URL sources and continue rendering titles as text rather than interpolated HTML.

Runnable example

<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Course finder</title>
<style>body{max-width:800px;margin:auto;padding:24px;font:18px/1.6 system-ui}input,select,button{font:inherit;max-width:100%}li{padding:16px;border-bottom:1px solid #ccc}label{display:block}</style>
<main><h1>Course finder</h1><label for="q">Keyword</label><input id="q" type="search"><label for="topic">Topic</label><select id="topic"><option value="all">All</option><option>HTML</option><option>CSS</option><option>JavaScript</option></select><button id="clear">Clear filters</button><p id="status" role="status"></p><ul id="results"></ul></main>
<script>
const courses=[{id:1,title:'Semantic HTML',topic:'HTML'},{id:2,title:'Forms and labels',topic:'HTML'},{id:3,title:'Grid layout',topic:'CSS'},{id:4,title:'Promises',topic:'JavaScript'}];
const query=document.querySelector('#q'),topic=document.querySelector('#topic'),list=document.querySelector('#results'),status=document.querySelector('#status');
function render(){
 const q=query.value.trim().toLowerCase();
 const items=courses.filter(x=>(topic.value==='all'||x.topic===topic.value)&&x.title.toLowerCase().includes(q));
 list.replaceChildren();
 for(const item of items){const li=document.createElement('li');li.textContent=`${item.title} — ${item.topic}`;list.append(li);}
 status.textContent=items.length?`${items.length} courses`:'No matches. Clear or change your filters.';
}
query.addEventListener('input',render);topic.addEventListener('change',render);
document.querySelector('#clear').addEventListener('click',()=>{query.value='';topic.value='all';render();query.focus();});render();
</script></html>

Trace and verify

Start with four; HTML gives two; forms narrows to one; grid gives none; clear restores four and focuses search. Uppercase PROMISES still matches. Repeated filtering must not mutate source data.

Common mistakes and boundaries

Do not add debounce to a small local search without need. A network version requires loading, errors, cancellation and stale-response handling. Empty results are not network failures and should have distinct feedback.

Practice challenge

Add a title-sort control, copying results before sorting and preserving combined filters.

Reference solution and reasoning

Sort a copied derived array with localeCompare. Treat ordering as a separate state and explicitly decide whether Clear resets it; implement that behavior consistently.

Further reading