Learning TypeScript - Build a todo
===

---
###### tags: `TypeScript`
# Mini project - Todo
## Setup
1. Create a project folder `[name]`.
2. `cd [name]` and create 2 folders `mkdir src dist` (this would create 2 folders inside project folder).
3. Run `tsc --init` to initialize(this would create a ts config json file).
4. Go inside of `tsconfig.json`, looking for `outdir` and change to ` "outDir": "./dist"`.
5. Go to bottom and add `"include:["src"]"`.
6. Open up terminal and run `tsc -w` to watch any changes.
7. If we want to see result, there're three ways of doing it
- Run `node` in the terminal.
- Creates a `index.html` and add `index.js `(compiled from `index.ts`) as script.
- Install a package called `light-server`
8. Create a `index.html` in root folder, add `index.js` in the `<script></script>`
9. Make sure to add `light-server` into `scripts` in `package.json`.
10. Run `npm run start`, light-server will be activated.



---
## Working with DOM
> Recap: What is DOM?
> **Document Object Model (DOM)** is a programming interface implemented by browsers in order to make static websites functional. The DOM API can be used to change the document structure, style, and content.
Here we are going to just make a simple demonstration,let's see how to find `btn` element.
```htmlembedded=
<body>
<h1>
Todo
</h1>
<ul></ul>
<form>
<input />
<button id="btn">
Add a Todo
</button>
</form>
</body>
```
In javaScript, we will need to use `document` and one of the properties called `getElementById`, it's the same in TypeScript, but what is the type of `document`?
Type `document` to see its type, and you will see that the type of `document` is `Document`.

If you want to check the definition, just hover to `document` and right click, click the `Go to Definition`, a file called `lib.dom.d.ts` will show up, you will see tons of information.

Ok, now we know that `document`'s type is `Document`, next we need property `getElementById` to help us target `button`, if you still have that file `lib.dom.d.ts` open, you can search for `getElementBtId`.

Here we know that the if we are going to use `getelementById`, we will need to pass an `id`, and it has to be a `string`.
```typescript=
const btn = document.getElementById('btn');
console.log(btn);
```
By default, TypeScript knows `DOM`, but how does it know? Let's look at the `tsconfig.json`, when you open the config file, there's a `lib`, by default it should be commented out, meaning we are using the default that includes the `DOM`.
Once we uncomment it, you can see that it shows error right away, meaning that TypeScript is no longer knows what is `DOM`.
What is `lib` doing here in the config file, it allows you to specify which JS features you can use, check the [document](https://www.typescriptlang.org/tsconfig#lib) here.

Let's say that we want to use `ES2021`, and we need to specify `DOM` as well, once we add these two, the error should be gone right away.
In short, if you need other features that need to be specified in `lib`, you will need to specified `DOM` as well.

---
### Non-null assertion
When we define a `btn` above, do you know what's its type? Hover over it and you will see it's `HTMLElment | null`. When we try to use `addEventListener`, it shows error immidiately because `btn` could be `HTMLElement` or `null`, what should we do to eliminate this situation?

#### Use optional chaining operator
By using optional chaining operator, it basically tells that run if type is `HTMLElement` or do nothing if it's `null`.

### Non-null assertion
What if I don't want to use questionmark operator? What I am certain that `btn` is 100% exsited.
We add an exclamation mark to tell that btn is there, it's not a `null`.

---
### Type assertion
In case you want to have more information about a value's type, you can assert a value's type by using `as` keyword, followed by the specific type.
```typescript=
const myPic = document.querySelector('profile-img');
const myPic = document.querySelector('profile-img') as HTMLImageElement;
```
Type assertion won't change the original type, it just specify more information in a certain context.
```typescript=
let mystery: unknown = 'Hello';
const numChar = (mystery as string).length
console.log(numChar) // 5
```
Even though we specify `mystery` type as `unknown`, we can still specify it as a `string` and using method that only a string can apply, we did not change the original type `unknown`.
---
### Situation that we might need type assertion
Let's say we want to type something and when we click the button, it will alert the texts, in the case, we need a `button` and a `input`.
```htmlembedded
<body>
<h1>
Todo
</h1>
<ul></ul>
<form>
<input id="todoInput" type="text" placeholder="Enter here"/>
<button id="btn">
Add a Todo
</button>
</form>
</body>
```
```typescript
const btn = document.getElementById('btn')!;
const input = document.getElementById('todoInput')!;
```
We added non-null assertion (!) because we know that these two are existed.
Then we want to add an event listener to listen the click event, and once we clicked, the input field will be empty.
```typescript
const btn = document.getElementById('btn')!;
const input = document.getElementById('todoInput')!;
btn.addEventListener('click', ()=>{
alert(input.value);
input.value = '';
})
```
Here's the problem though, it shows that `value` doesn't exist on type `HTMLElement`, let's break down.

1. Hover over `btn` and `input` to see its type, both are type `HTMLElement`.


2. open your devtool, go to `console` and type `console.dir(input)` and expend it, scroll down to last, you can see `HTMLIputElement`, this is the type we are going to assert, because TypeScript also uses this as its interface, same as `button`.

Let's use type assertion in this case.
```typescript
const btn = document.getElementById('btn')! as HTMLButtonElement;
const input = document.getElementById('todoInput')! as HTMLInputElement;
btn.addEventListener('click', ()=>{
alert(input.value);
input.value = '';
})
```

Here we've successfully implemented type assertion for `btn` and `input`.
If we only want to add an eventlistener to `button`, we don't necessary have to use type assertion because `button` works fine with `HTMLElement`, but why not be more specific.
---
### Working with event
Now let's try to submit form.
```htmlembedded!
<body>
<h1>
Todo
</h1>
<ul></ul>
<form>
<input id="todoInput" type="text" placeholder="Enter here"/>
<button id="btn" type="submit">
Add a Todo
</button>
</form>
</body>
```
```typescript!
const btn = document.getElementById('btn')! as HTMLButtonElement;
const input = document.getElementById('todoInput')! as HTMLInputElement;
const form = document.querySelector('form')!
.addEventListener('submit', (e)=>{
e.preventDefault();
console.log('Submitted!')
})
```

TypeScript knows that the `e` type is a `SubmitEvent` in this context automatically if you have over `e`, what if we make it seperated? Does TypeScript still knows the type of `e`?

Apparently TypeScript does not know the type of `e` anymore when out of the context, so we need to annotate the type in order to make it works.

---
### Let's building our todo.
First I'd like to think the building steps.
1. A form that allows us to submit, a callback function to handle submition.
2. function will do a series of actions below:
- Prevent submition if there's no value in the input field.
- Get the `input` value.
- Get `ul`.
- Create `li`
- Create a `checkbox`
- Append checkbox to `li`
- Append `li` to `ul`
```htmlembedded!
<body>
<h1>
Todo
</h1>
<ul></ul>
<form>
<input id="todoInput" type="text" placeholder="Enter here"/>
<button id="btn" type="submit">
Add a Todo
</button>
<ul id="todolist"></ul>
</form>
</body>
```
```typescript!
const btn = document.getElementById('btn')! as HTMLButtonElement;
const input = document.getElementById('todoInput')! as HTMLInputElement;
const form = document.querySelector('form')!
// Get `ul`
const list = document.getElementById('todolist');
const handleSubmit = (e: SubmitEvent) => {
e.preventDefault();
// Get the input value
const newTodoText = input.value;
// Create li
const newList = document.createElement('li');
// Create checkbox
const checkbox = document.createElement('input');
// assign type
checkbox.type = 'checkbox';
// append newTodoText to li
newList.append(newTodoText);
// append checkbox to li
nexList.append(checkbox);
// append li to ul
list.append(newList);
// Set input field empty
input.value = '';
}
form.addEventListener('submit', handleSubmit);
```
---
### Add interface
Because we didn't store these todos into database or localstorage, todos will be gone after refreshing the page, and they are just lists, it's not a collection of data, so if we want to store them in localstorage, it has to be in an array as a data structure.
First we need to create / define the alias or interface as an entity.
> [Interface - Chinese](https://pjchender.dev/typescript/ts-interface/)
> [Interface - En](https://www.typescripttutorial.net/typescript-tutorial/typescript-interface/)
```typescript!
// create an interface
interface Todo {
text: string;
completed: boolean;
}
const todos: Todo[] = [];
const btn = document.getElementById('btn')! as HTMLButtonElement;
const input = document.getElementById('todoinput')! as HTMLInputElement;
const form = document.querySelector('form')!;
const list = document.getElementById('todolist')!;
// make 2 functions
// 1 for handling submition
const handleSubmit = (e: SubmitEvent) => {
const newTodo: Todo = {
text: input.value,
completed: false
}
createTodo(newTodo);
todo.push(newTodo);
input.value = '';
}
// 2 for create element
const createTodo = (todo: Todo) => {
const newList = document.createElement('li');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
newList.append(todo.text);
newList.append(checkbox);
list.append(newList);
}
form.addEventListener('submit', handleSubmit);
```
---
### Connecting to localStorage
Using `localStorage.setItem()` allows us to store items in the localstorage.
Remember that everything we store in the localstorage should be string, therefore, we need to use `JSON.stringify()`.
```typescript=
let todo = [];
localStorage.setItem('todos', JSON.stringify(todos))
```
Once we've setted the key(`todos`) in the localstorage, we can use `localStorage.getItem()` to track all the todo if any.
Let's create a function that handles reading todos in there're any in the localstorage, but first let's take a look at our code before.
```typescript=
interface Todo {
text: string;
completed: boolean;
}
const todos: Todo[] = [];
const btn = document.getElementById('btn')! as HTMLButtonElement;
const input = document.getElementById('todoinput')! as HTMLInputElement;
const form = document.querySelector('form')!;
const list = document.getElementById('todolist')!;
```
Here we can the `const todos: Todo[] =[];`, here we already annotated type of todos, when we create that function of reading the existed todos, if we don't annotate the type, it would be type `any`, instead of letting to be type `any`, we can annotate to be `Todo[]`


Here whenever we call function `readTodo`, it will always be an array; since the function is handling call out all the todos if any, we replace `const todos: Todo[] = []` with `const todos: Todo[] = readTodo();`
This mean that when the page loaded, it will immidiately invoke `readTodo()` and show `todos`, let's see in the console.

---
### Showing todos on the page
Once we've got todos(let's assume we already have), we can loop through it(**because it's an array**) and invoke function `createTodo` to actually create the item.
```typescript=
const todos:Todo[] = readTodos();
todos.forEach(createTodo);
```
### **Fixing error of scope**
```typescript=
interface Todo {
text: string;
completed: boolean;
}
const todos: Todo[] = readTodos();
todos.forEach(createTodo);
const btn = document.getElementById('btn')! as HTMLButtonElement;
const input = document.getElementById('todoinput')! as HTMLInputElement;
const form = document.querySelector('form')!;
const list = document.getElementById('todolist')!;
const readTodos = (): Todo[] => {
const todoItems = localStorage.getItem('todos');
if(todoItems === null) return [];
return JSON.parse(todoItems);
}
const handleSubmit = (e:SubmitEvent) => {
...
}
const createTodo = (todo: Todo) => {
const newList = document.createElement('li');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
newList.append(todo.text);
newList.append(checkbox);
list.append(newList);
}
form.addEventListener('submit', handleSubmit);
```
From code above, what do you think which part causes errors?
**Answer:**
The errors occurred when I used **arrow function** to create `readTodo()` and `createTodo()`, why?
**Arrow function is an new format of function expression, and function expression does not hoisted**, therefore, when I declared `readTodos()` and `createTodo()` by using arrow function, but under the interface, we're already invoking `readTodo` and `createTodo`, but computer does not know what they are.
How do we fix it?
1. Move `const todo: Todo[] = readTodos();` under the `readTodos()` function and so does `todos.forEach(createTodo);`
2. Change arrow function to function declaration.
#### Approach 1
```typescript=
const readTodos = (): Todo[] => {
const todoItems = localStorage.getItem('todos');
if(todoItems === null) return [];
return JSON.parse(todoItems);
}
const todos:Todo[] = readTodos();
const createTodo = (todo: Todo) => {
const newList = document.createElement('li');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
newList.append(todo.text);
newList.append(checkbox);
list.append(newList);
}
todos.forEach(createTodo);
```
#### Approach 2
```typescript=
function readTodos(): Todo[]{
const todoItems = localStorage.getItem('todos');
if(todoItems === null) return [];
return JSON.parse(todoItems);
}
function createTodo(todo: Todo){
const newList = document.createElement('li');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
newList.append(todo.text);
newList.append(checkbox);
list.append(newList);
}
```
Ok, I've chosen approach 2, but todos did not show up on the screen, why? Let's see what happend?

```typescript=
const todos:Todo[] = readTodos();
todos.forEach(createTodo);
const btn = document.getElementById('btn')! as HTMLButtonElement;
const input = document.getElementById('todoinput')! as HTMLInputElement;
const form = document.querySelector('form')!;
const list = document.getElementById('todolist')!;
```
The error occurred because of We invoked `createTodo` before initializing all the variables. This is an easy fix, just move down `todos.forEach(createTodo);` under the `const list = document.getElementById('todolist')!;`
```typescript=
const todos:Todo[] = readTodos();
const btn = document.getElementById('btn')! as HTMLButtonElement;
const input = document.getElementById('todoinput')! as HTMLInputElement;
const form = document.querySelector('form')!;
const list = document.getElementById('todolist')!;
todos.forEach(createTodo);
```
---
### Updating completed status
Now we can save todos and call out them from local storage, next we need to be able to update the completed status.
How do we proceed? First of all, we need to add an event listener to listen a `change` event.
We can add this event into our `createTodo` function simply because when the page first loaded, it should show all todos with its completed status, hence, we also want to store the status in the localstorage.
```typescript=
function createTodo (todo: Todo) {
const newList = document.createElement('li');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
// here we refers the status as value boolean
checkbox.checked = todo.completed;
checkbox.addEventListener('change', () => {
todo.completed = checkbox.checked;
saveTodos();
})
newList.append(todo.text);
newList.append(checkbox);
list.append(newList);
}
```