It's time to take care of filtering.
Open index.html and modify the todoFilter input as follows:
<input type="text" id="todoFilter" title="Filter for the todo list" onkeyup="filterTodoList()" onblur="filterTodoList()" />
As you might guess, the next step is to implement the filterTodoList function in TypeScript.
Here's an implementation of that function:
function filterTodoList(): void { console.log("Filtering the rendered todo list"); const todoListHtml: HTMLUListElement =
document.getElementById('todoList') as HTMLUListElement; if (todoListHtml === null) { console.log("Nothing to filter"); return; } const todoListFilter = document.getElementById('todoFilter') as
HTMLInputElement; const todoListFilterText = todoListFilter.value.toUpperCase(); todoListHtml.childNodes.forEach((item) => { let itemText: string | null = item.textContent; if (itemText !== null) { itemText = itemText.toUpperCase(); if (itemText.startsWith(todoListFilterText)) { (item as HTMLLIElement).style.display = "list-item"; } else { (item as HTMLLIElement).style.display = "none"; } } }); }
There are a few interesting things to explain about that code, but first, you also need to modify the addTodo function to invoke filterTodoList right after updateTodoList:
if ('' !== newTodo.trim()) { ... // update the todo list updateTodoList(); // apply the todo list filter filterTodoList(); }
First of all, if you simply add it to todo-it.ts and compile it, note that you'll make TypeScript angry:
todo-it.ts:97:25 - error TS2339: Property 'startsWith' does not exist on type 'string'. 97 if(itemText.startsWith(todoListFilterText)) {
Can you guess why you're getting this compilation error?
startsWith(...) is a function that was introduced in ES2015, but so far, in our project, we have targeted ES5. TypeScript can help us because, as we said earlier, it includes a set of libraries/type definitions for the DOM and some other things. One of these libraries is the ES2015 library.
The set of libraries that are included by default are defined by the target option in tsconfig.json. In our case, with ES5, the following libraries are included: DOM, ES5, and ScriptHost.
To fix the compilation issue, edit the tsconfig.json file of your project, uncomment the lib option, and set its value as follows: "lib": ["es2015", "dom", "scripthost"],.
As soon as you do so, the compiler should stop complaining. Now, we need to take a step back and clarify what we have done.
The target option in tsconfig.json basically tells TypeScript which language features should be down-level emitted, while the lib option tells TypeScript what is supported by the target environment. Thus, it is logical that if we are targeting ES5, ES2015 features such as the startsWith method will not be available.
By modifying the lib property, we have told TypeScript that it is okay to let us use some of the ES2015 features directly. But doing so means that the compiled code will not be changed. If you look at the generated JS code, you'll see that, indeed, the startsWith call remains there.
This should be safe as most modern web browsers already support ES2015 to a very large extent. Although, if you want to maintain compatibility with older execution environments, then you need to learn about shims and polyfills. They are libraries that you can add to your project in order to really have access to newer features in an environment that does not provide them by default. We might touch on this again later in the book, but probably not in detail.
Make sure to check out the home page of the core-js project (https://github.com/zloirock/core-js), as it provides polyfills for many features. If you want a good exercise for later, then try the following:
- Add core-js to your project dependencies.
- Import the starts-with feature using import 'core-js/features/string/starts-with'.
- Compile and test it in an old browser.
Another interesting point about the filterTodoList function is that it explicitly marks the itemText variable as nullable using let itemText: string | null. Without this, TypeScript wouldn't let you compile the code since the value could indeed be null.
Once this is done though, it is impossible to simply call methods such as toUpperCase() on the itemText variable. Before doing so, a null check is required for type safety. Again, TypeScript type inference helps a lot since, within the if (itemText !== null) { ... } block, TypeScript knows that itemText is not null.
If we focus on what the code actually does, then it can be summarized as follows:
- Loops over each entry (li) in the displayed list (ul)
- For each item, it does the following:
- Gets its text and changes it to uppercase
- Checks whether the item text starts with the uppercase filter text:
- If it does, then it sets the default list-item style on it
- Otherwise, it hides the item using the none display style