VanillaJS - 初心者こそ、Docker を使おう！　

# 初心者こそ、Dockerを使おうの 素の JavaScript 編です。
多分一番ボリュームがあります。
* 素のJSという割には、Parcel, Pug, TypeScript 等を使っていますのでここでは、Vue.js や　React 等を使わないでという意味で解釈ください。


このTodoは、JavaScript Primer さんの Todo を参考にしています。
`ｃlass` を使って実践的な`Todo`が学べますので、ぜひ覗いてみてください.
* https://jsprimer.net/use-case/todoapp

* fileの構成予定

```sh
├── docker-compose.yml
└── src
    ├── App.ts
    ├── EventEmitter.ts
    ├── TodoItemModel.ts
    ├── dist
    ├── html-util.ts
    ├── index.pug
    ├── index.ts
    ├── package.json
    ├── style.stylus
    └── yarn.lock
```


* 同じディレクトリで作業をするなら、データのコピーはもう済んでいるので `docker-compose.yml` の`working_dir`, `command`の `#`を差し替えると、`docker-compose up -d` でバックグラウンドで `Parcel` がたちあがるはずです。
`docker-compose ps` で確認してみてください.
バックグラウンドのコンテナに入るコマンドは　`docker-compose exec node bash` です

## 00

`docker-compose.yml`

```
version: "3"
services:
  node:
    container_name: node
    image: atoris1192/node:0.1.5
    # build: .
    # volumes は上書きに注意
    volumes:
      - .:/app
    ports:
      - "1234:1234"
      - "1235:1235"
    # working_dir: /app
    working_dir: /app/src
    # command: cp -rp /tmp/src /app
    command: npx parcel --hmr-port 1235 --hmr-hostname localhost index.pug
    tty: true
```

`src/index.pug`

```ts
<!DOCTYPE html>
html(lang="en")
  head
    meta(charset="UTF-8")
    meta(name="viewport", content="width=device-width, initial-scale=1.0")
    meta(http-equiv="X-UA-Compatible", content="ie=edge")
    title Document
    link(rel="stylesheet", href="style.stylus")
  body
    .todoapp
      form#js-form(action="")
        input#js-form-input.new-todo(type="text" placeholder="new todo" autocomplete="off")

      #js-todo-list.todo-list

      footer.footer
        span#js-todo-count TodoItems: 0
    script(src="./index.ts")
```
`index.ts`

```ts
import { App } from './App';

const app = new App()
document.addEventListener('DOMContentLoaded', () => {
  app.main();
})
```

`App.ts`

```ts
export class App {
  constructor() {
    console.log("App init ...");
  }
  main() {
    console.log("App ...");
  }
}
```

`style.stylus`

```css
html,
body {
    margin: 0;
    padding: 0;
}

button {
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    font-size: 100%;
    vertical-align: baseline;
}

body {
    font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
    line-height: 1.4em;
    background: #f5f5f5;
    color: #4d4d4d;
    min-width: 230px;
    max-width: 550px;
    margin: 0 auto;
    font-weight: 300;
}

:focus {
    outline: 0;
}

.todoapp {
    background: #fff;
    margin: 130px 0 40px 0;
    position: relative;
    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
    0 25px 50px 0 rgba(0, 0, 0, 0.1);
    min-height: 150px;
}


.todoapp input::placeholder {
    font-style: italic;
    font-weight: 300;
}

.new-todo {
    position: relative;
    margin: 0;
    width: 100%;
    font-size: 24px;
    line-height: 1.4em;
    border: 0;
    color: inherit;
    box-sizing: border-box;
    padding: 16px 16px 16px 60px;
    border: none;
    background: rgba(0, 0, 0, 0.003);
    box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}

.todo-list ul {
    margin: 0;
    padding: 0;
    list-style: none;
}

.todo-list li {
    position: relative;
    font-size: 24px;
    border-bottom: 1px solid #ededed;
    padding: 16px;
}

.todo-list li:last-child {
    border-bottom: none;
}

.todo-list li input[type="checkbox"] {
    width: 40px;
    height: auto;
    margin: auto 0;
    border: none;
}

.todo-list li .delete {
    position: absolute;
    top: 0;
    right: 10px;
    bottom: 0;
    width: 40px;
    height: 40px;
    color: #cc9a9a;
}

.footer {
    color: #777;
    height: 20px;
    padding: 10px 15px;
    border-top: 1px solid #e6e6e6;
}
```

## Parcel 確認
* `docker-compose.yml` を書き換えている人は、もうすでに Parel が立ち上がっているので以下は必要ありません。


```sh
docker-compose up
docker-comppse ps
docker-compose run --service-port node bash
uname
cd src
npx parcel --hmr-port 1235 --hmr-hostname localhost index.pug
```


## 描画処理 App.ts に書いていきます

`App.ts`

記述は、html 文字列で書いて、処理は、htmlエレメントがほしいので、とりあえず関数化`htmlElement()`しておきます。

処理は、 main 関数に書いていきます。描画は render に分けておきます


```ts
const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]

export class App {
  constructor() {

  }
  render() {
    const jsTodoList = document.querySelector('#js-todo-list');

    function htmlElement(todos) {
      const ul = document.createElement('ul');
      const template = document.createElement('template');

      todos.forEach( todo => {
        const li = `<li>${ todo.title } : ${ todo.isDone } : ${ todo.id}</li>`
        template.innerHTML = li;
        ul.appendChild(template.content.firstElementChild)
      })
      return ul;
    }
    // 配列 -> htmlエレメント
    const todosElement = htmlElement(todos);

    jsTodoList.textContent = '';
    jsTodoList.appendChild(todosElement);

  }
  main() {
    console.log("App ...");

    this.render();

  }
}
```

## input 処理

`App.ts`

```ts
const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]

export class App {
  constructor() {
    console.log("App init ...");
  }
  render() {
    const jsTodoList = document.querySelector('#js-todo-list');

    function htmlElement(todos) {
      const ul = document.createElement('ul');
      const template = document.createElement('template');

      todos.forEach( todo => {
        const li = `<li>${ todo.title } : ${ todo.isDone } : ${ todo.id}</li>`
        template.innerHTML = li;
        ul.appendChild(template.content.firstElementChild)
      })
      return ul;
    }
    // 配列 -> htmlエレメント
    const todosElement = htmlElement(todos);

    jsTodoList.textContent = '';
    jsTodoList.appendChild(todosElement);

  }
  main() {
    // input 処理
    const jsForm = document.querySelector('#js-form');

    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      const jsFormInput: any = document.querySelector('#js-form-input');

      interface Item {
        id: number;
        title: string;
        isDone: boolean;
      }

      const item: Item = {
        id: new Date().getTime(),
        title: jsFormInput.value,
        isDone: false,
      }

      todos.push(item);
      jsFormInput.value = '';
      this.render()
    })


    this.render();

  }
}
```

## HTMLエレメント処理を、外部ファイルにする
`javascript` の `html` 処理は、実に面倒くさいのですが、`JavaScript Primer`さんの`html-util`をそのまま使わせていただきます。

タグ付きテンプレート文字列を使って、スマートに変換してくれます。
おまけにエスケープ処理までしてくれますので、これを使わない理由がありません。
詳しい使い方、`JavaScript Primer`さんをチェックしてください。
* element`<li>${ todo.id }</li>` この文字列タグをhtmlエレメントタグに変換してくれます。

これを使って書き換えます

`html-util.ts`


```ts

function escapeSpecialChars(str: string) {
  return str
    .replace(/&/g, "&apm:")
    .replace(/</g, "&lt;")
    .replace(/>/g, "%gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

// htmlString -> htmlElement DOM Node
function htmlToElement(html: string): any {
  const template = document.createElement('template') ;
  template.innerHTML = html;
  return template.content.firstElementChild;
}

// escape + DOM Node
export function element(strings, ...values) {
  const htmlString = strings.reduce((result, str, i) => {
    const value = values[i - 1];
    if (typeof value === "string") {
      return result + escapeSpecialChars(value) + str;
    } else {
      return result + String(value) + str;
    }
  })
  return htmlToElement(htmlString);
}

// Add child Element
export function render(bodyElement, containerElement) {
  containerElement.innerHTML = ''; // 一旦全削除
  containerElement.appendChild(bodyElement);
}

```

* コード量は、今はあまり変わりませんが、エスケープ処理もしてくれていますし、htmlエレメントを意識しなくてよくなるので書きやすくなります。

`App.ts`

```ts
import { element, render } from './html-util';

const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]

export class App {
  constructor() {
    console.log("App init ...");
  }
  render() {
    const jsTodoList = document.querySelector('#js-todo-list');
    const ul = element`<ul />`

    todos.forEach( todo => {
      const li = element`<li>${ todo.title } : ${ todo.isDone } : ${ todo.id}</li>`
      ul.appendChild(li)
    })

    render(ul, jsTodoList);
    // jsTodoList.textContent = '';
    // jsTodoList.appendChild(ul);

  }
  main() {
    // input 処理
    const jsForm = document.querySelector('#js-form');

    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      const jsFormInput: any = document.querySelector('#js-form-input');

      interface Item {
        id: number;
        title: string;
        isDone: boolean;
      }

      const item: Item = {
        id: new Date().getTime(),
        title: jsFormInput.value,
        isDone: false,
      }

      todos.push(item);
      jsFormInput.value = '';
      this.render()
    })


    this.render();

  }
}
```

## data 処理用クラスを作る `todoLiseModel`

todo データを切ったり貼ったりするメソッドは、すべて`todoListModel` に集約させたいと思います。

`App.ts`

```ts
import { element, render } from './html-util';

const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]
class TodoListModel {  // todo data 処理用
  private todos: any;
  constructor(todos = []) {
    this.todos = todos // 初期化
  }
  getTodos() {
    return this.todos;
  }
  setTodos(todos) {
    this.todos = todos;
  }
  getTotalCount() {
    return this.todos.length;
  }
  addTodo({ title }) {
    interface Item {
      id: number;
      title: string;
      isDone: boolean;
    }
    const item: Item = {
      id: new Date().getTime(),
      title: title,
      isDone: false,
    }
    this.todos.push(item);
  }
}

export class App {
  private todoListModel: any;
  constructor() {
    this.todoListModel = new TodoListModel(todos) // インスタンス作成 仮データセット
  }

  render() {
    const jsTodoList = document.querySelector('#js-todo-list');
    const ul = element`<ul />`
    todos.forEach( todo => {
      const li = element`<li>${ todo.title } : ${ todo.isDone } : ${ todo.id}</li>`
      ul.appendChild(li)
    })
    render(ul,jsTodoList);
  }

  main() {
    const jsForm = document.querySelector('#js-form');

    // input 処理
    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      const jsFormInput: any = document.querySelector('#js-form-input');
      if(!jsFormInput.value.trim()) return

      this.todoListModel.addTodo({
        title: jsFormInput.value,
      })
      jsFormInput.value = '';
      this.render()
    })

    this.render();
  }
}
```

## データの定義クラスを作る `todoItemModel`
`todoListModel` で todoデータの処理を担当してもらいましたが、定義もそのなかに含まれています。
それを新たなクラスで管理したいと思います
これで、機能の分担化ができました。

`App.ts`

```ts
import { element, render } from './html-util';

const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]

class TodoItemModel { // item 定義用
  private id: number;
  private title: string;
  private isDone: boolean;
  constructor({ title }) {
    this.id = new Date().getTime()
    this.title = title
    this.isDone = false
  }
}

class TodoListModel {  // todo data 処理用
  private todos: any;
  constructor(todos = []) {
    this.todos = todos // 初期化
  }
  getTodos() {
    return this.todos;
  }
  setTodos(todos) {
    this.todos = todos;
  }
  getTotalCoount() {
    this.todos.length;
  }
  addTodo( item ) {
    this.todos.push(item);
  }
  // addTodo({ title }) {
  //   interface Item {
  //     id: number;
  //     title: string;
  //     isDone: boolean;
  //   }
  //   const item: Item = {
  //     id: new Date().getTime(),
  //     title: title,
  //     isDone: false,
  //   }
  //   this.todos.push(item);
  // }
}

export class App {
  private todoListModel: any;
  constructor() {
    this.todoListModel = new TodoListModel(todos) // インスタンス作成 仮データセット
    // this.todoListModel_2 = new TodoListModel() // インスタンスなのでいくつでも必要個数作成できる
  }

  render() {
    const jsTodoList = document.querySelector('#js-todo-list');
    const ul = element`<ul />`
    todos.forEach( todo => {
      const li = element`<li>${ todo.title } : ${ todo.isDone } : ${ todo.id}</li>`
      ul.appendChild(li)
    })
    render(ul,jsTodoList);
  }

  main() {
    const jsForm = document.querySelector('#js-form');

    // input 処理
    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      const jsFormInput: any = document.querySelector('#js-form-input');
      if(!jsFormInput.value.trim()) return

      this.todoListModel.addTodo( new TodoItemModel({ // インスタンス作成　使い捨てです。
        title: jsFormInput.value,
      }))
      // this.todoListModel.addTodo({
      //   title: jsFormInput.value,
      // })
      jsFormInput.value = '';
      this.render()
    })

    this.render();
  }
}
```

## 独自イベント　を作る
これは、予め用意されたイベント以外に、独自にイベントの発行とリスナーを作ります。
用意されたイベントというのは、ボタンをクリックしたらとか、チェックボックスの状態が変わったらなど条件検知で命令を実行するイベントリスナーを自分で作ってしまいます。
 今回は、`todo` データの状態が変わったら、実行するというイベント用になります。
差し替えるメソッドは、`this.render()` と差し替える形をとりますが、このイベントリスナーは、今回でいうとそんなに恩恵があるわけではありません。　`this.render()` で特に不便はないのですがイベント検知の考え方は、より汎用性の高い使い方ができそうです。　イメージで言えば、ライトが着いたらとか、温度が規定温度になったら実行するというようなイベントを検知した時点で実行されるものなので、応用が効くと思われます。

これも、拝借してきたものをそのまま使います。
中身をみると、Map関数にイベント用のキーと関数を入れる器が用意されているだけです。
これを、`TodoLiistModel` の親要素のして継承させます。
そうすることによって、`TodoListModel` にはメソッドを書かなくても使えるようになります。

`src/EventEmitter.ts`

```ts
export class EventEmitter {
  private _listeners: any;
  constructor() {
    // イベント名、リスナー関数が入る
    this._listeners = new Map();
  }
  addEventListener(type:string, listener:any) {
    if (!this._listeners.has(type)) {
      this._listeners.set(type, new Set());
    }
    const listenerSet = this._listeners.get(type);
    listenerSet.add(listener);
  }
  emit(type:string) {
    const listenerSet = this._listeners.get(type)
    if (!listenerSet) {
      return;
    }
    listenerSet.forEach(listener => {
      listener.call(this);
    })
  }
  removeEventListener(type:string, listener:any) {
    const listenerSet = this._listeners.get(type);
    if (!listenerSet) {
      return;
    }
    listenerSet.forEach(ownListener => {
      if (ownListener === listener) {
        listenerSet.delete(listener);
      }
    })
  }
}
```

`src/App.ts`

```ts
import { element, render } from './html-util';
import { TodoItemModel } from './TodoItemModel';
import { EventEmitter } from './EventEmitter';

const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]

// EventEmitter を　追加
class TodoListModel extends EventEmitter {  // todo data 処理用
  private todos: any[];
  constructor(todos = []) {
    super();
    this.todos = todos // 初期化
  }
  getTodos() {
    return this.todos;
  }
  setTodos(todos) {
    this.todos = todos;
  }
  getTotalCoount() {
    this.todos.length;
  }
  addTodo( item ) {
    this.todos.push(item);
  }
}

export class App {
  private todoListModel: any;
  constructor() {
    this.todoListModel = new TodoListModel(todos) // インスタンス作成 仮データセット
  }

  render() {
    const jsTodoList = document.querySelector('#js-todo-list');
    const ul = element`<ul />`
    todos.forEach( todo => {
      const li = element`<li>${ todo.title } : ${ todo.isDone } : ${ todo.id}</li>`
      ul.appendChild(li)
    })
    render(ul,jsTodoList);
  }

  main() {
    const jsForm = document.querySelector('#js-form');

    // change イベントリスナー関数
    this.todoListModel.addEventListener('change', this.render)

    // input 処理
    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      const jsFormInput: any = document.querySelector('#js-form-input');
      if(!jsFormInput.value.trim()) return

      this.todoListModel.addTodo( new TodoItemModel({ // インスタンス作成
        title: jsFormInput.value,
      }))
      jsFormInput.value = '';
      this.todoListModel.emit('change'); // changeイベントエミッター
      // this.render()
    })
    this.todoListModel.emit('change'); // changeイベントエミッター
    // this.render();
  }
}
```

* これは、もう動きがないので別ファイルにして置きます。

`src/TodoItemModel.ts`

```ts
export class TodoItemModel { // item 定義用
  private id: number;
  private title: string;
  private isDone: boolean;
  constructor({ title }) {
    this.id = new Date().getTime()
    this.title = title
    this.isDone = false
  }
}
```

`App.ts` をリファクタリングして`main`関数を整理しておきます

```ts
import { element, render } from './html-util';
import { TodoItemModel } from './TodoItemModel';
import { EventEmitter } from './EventEmitter';

const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]

class TodoListModel extends EventEmitter {  
  private todos: any[];
  constructor(todos = []) {
    super();
    this.todos = todos
  }
  getTodos() {
    return this.todos;
  }
  setTodos(todos) {
    this.todos = todos;
  }
  getTotalCount() {
    return this.todos.length;
  }
  addTodo( item ) {
    this.todos.push(item);
  }
}

export class App {
  private todoListModel: any;
  constructor() {
    this.todoListModel = new TodoListModel(todos)
  }
  totalCount(totalCount) {
    const jsTodoCount = document.querySelector('#js-todo-count');
    jsTodoCount.textContent = `TodoItems: ${ totalCount }`
  }
  inputTodo() {
      const jsFormInput: any = document.querySelector('#js-form-input');
      if(!jsFormInput.value.trim()) return

      this.todoListModel.addTodo( new TodoItemModel({
        title: jsFormInput.value,
      }))
      jsFormInput.value = '';
      this.todoListModel.emit('change');
  }
  render() {
    const jsTodoList = document.querySelector('#js-todo-list');
    const ul = element`<ul />`
    todos.forEach( todo => {
      const li = element`<li>${ todo.title } : ${ todo.isDone } : ${ todo.id}</li>`
      ul.appendChild(li)
    })
    render(ul,jsTodoList);
  }

  main() {
    const jsForm = document.querySelector('#js-form');

    this.todoListModel.addEventListener('change', this.render)  // リスナーは複数立てれる
    this.todoListModel.addEventListener('change', () => {
      const totalCount = this.todoListModel.getTotalCount();
      this.totalCount(totalCount)
    })

    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      this.inputTodo();
    });
    this.todoListModel.emit('change');
  }
}
```

## 後は、削除や、チェックボックス, パージ処理を書いていきます

`App.ts`

```ts
import { element, render } from './html-util';
import { TodoItemModel } from './TodoItemModel';
import { EventEmitter } from './EventEmitter';

const todos = [
  { id: 0, title: "task0", isDone: false },
  { id: 1, title: "task1", isDone: true },
  { id: 2, title: "task2", isDone: false },
]

class TodoListModel extends EventEmitter {  
  private todos: any[];
  constructor(todos = []) {
    super();
    this.todos = todos
  }
  getTodos() {
    return this.todos;
  }
  setTodos(todos) {
    this.todos = todos;
  }
  getTotalCount() {
    return this.todos.length;
  }
  addTodo( item ) {
    this.todos.push(item);
  }
}

export class App {
  private todoListModel: any;
  constructor() {
    this.todoListModel = new TodoListModel(todos)
  }
  purge() { // 完了分全削除
    const todos = this.todoListModel.getTodos();
    const newTodos = todos.filter( todo => {
      return !todo.isDone
    });
    this.todoListModel.setTodos(newTodos);
    this.todoListModel.emit('change');
  }
  totalCount() {
    const jsTodoCount = document.querySelector('#js-todo-count');
    const totalCount = this.todoListModel.getTotalCount()
    jsTodoCount.textContent = `TodoItems: ${ totalCount }`
  }
  inputTodo() {
      const jsFormInput: any = document.querySelector('#js-form-input');
      if(!jsFormInput.value.trim()) return

      this.todoListModel.addTodo( new TodoItemModel({
        title: jsFormInput.value,
      }))
      jsFormInput.value = '';
      this.todoListModel.emit('change');
  }
  render() {
    const jsTodoList = document.querySelector('#js-todo-list');
    const todos = this.todoListModel.getTodos()

    const ul = element`<ul />`
    todos.forEach( todo => {
      const li = todo.isDone
        ? element`<li><input type="checkbox" class="checkbox" checked/><del>${ todo.title } : ${ todo.isDone } : ${ todo.id}</del><button class="delete">[x]</button></li>`
        : element`<li><input type="checkbox" class="checkbox" />${ todo.title } : ${ todo.isDone } : ${ todo.id}<button class="delete">[x]</button></li>`

      // チェックボックス状態
      const checkboxState = li.querySelector('.checkbox');
      checkboxState.addEventListener('change', () => {
       const todos = this.todoListModel.getTodos();
       const item = todos.find( item => {
         return item.id === todo.id;
       })
       item.isDone = !item.isDone
       this.todoListModel.emit('change');
      })

      // 削除処理
      const deleteBtn = li.querySelector('.delete');
      deleteBtn.addEventListener('click', () => {
       const todos = this.todoListModel.getTodos();
       const pos = todos.map( todo => {
         return todo.id
       }).indexOf(todo.id);
       todos.splice(pos, 1)
       this.todoListModel.emit('change');
      })

      ul.appendChild(li)
    })
    render(ul,jsTodoList);
  }

  main() {
    const jsForm = document.querySelector('#js-form');
    const purge = document.querySelector('#purge');

    purge.addEventListener('click', () => {
      this.purge();
    })
    // changeリスナー関数
    this.todoListModel.addEventListener('change', () => {
      this.render();
    })  
    this.todoListModel.addEventListener('change', () => { // 分ける必要は無いが、説明の為分けている
      this.totalCount()
    })

    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      this.inputTodo();
    });
    this.todoListModel.emit('change');
  }
}
```

## FireStore
このままでは、リロードでデータが消えてしまうので、FireStoreに接続させます

詳しくは、`vue.js`編のデータの永続化の投稿を参照ください。

firebase関係のエラーがでていたら、コンテナに入って

---
uname
cd src
yarn add Firebase
npx parcel --hmr-port 1235 --hmr-hostname localhost index.pug

---

ポートマッピングで二重起動していると失敗しますので、注意してください。


`App.ts`

```ts
import { element, render } from './html-util';
import { TodoItemModel } from './TodoItemModel';
import { EventEmitter } from './EventEmitter';
import * as firebase from 'firebase/app';
import 'firebase/firestore';
import { config } from '../firebase';

firebase.initializeApp(config);
const db = firebase.firestore()
const collection = db.collection('todos')


class TodoListModel extends EventEmitter {  
  private todos: any[];
  constructor(todos = []) {
    super();
    this.todos = todos
  }
  getTodos() {
    return this.todos;
  }
  setTodos(todos) {
    this.todos = todos;
  }
  getTotalCount() {
    return this.todos.length;
  }
  async addTodo({ title }) {
    interface Item  {
      id: number;
      titel: string;
      isDone: boolean;
      created_at: any;
    }
    const item = {
      id: new Date().getTime(),
      title: title,
      isDone: false,
      created_at: firebase.firestore.FieldValue.serverTimestamp(),
    }
    await collection.add(item)
      .then(result => {
        console.log(result);
      })
      .catch(err => console.log(err)
      )
  }
  // addTodo( item ) {
  //   this.todos.push(item);
  // }
}

export class App {
  private todoListModel: any;
  constructor() {
    this.todoListModel = new TodoListModel()
  }
  async purge() { // 完了分全削除

    // const todos = this.todoListModel.getTodos();
    // const newTodos = todos.filter( todo => {
    //   return !todo.isDone
    // });
    // this.todoListModel.setTodos(newTodos);
    await collection.where('isDone', '==', true)
      .get()
      .then(snapshot => {
        snapshot.forEach( doc => {
          collection.doc(doc.id).delete();
        })
      })
      .catch(err => console.log(err))
    this.todoListModel.emit('change');
  }
  totalCount() {
    const jsTodoCount = document.querySelector('#js-todo-count');
    const totalCount = this.todoListModel.getTotalCount()
    jsTodoCount.textContent = `TodoItems: ${ totalCount }`
  }
  inputTodo() {
      const jsFormInput: any = document.querySelector('#js-form-input');
      if(!jsFormInput.value.trim()) return

      this.todoListModel.addTodo({
        title: jsFormInput.value,
      });
      // this.todoListModel.addTodo( new TodoItemModel({
      //   title: jsFormInput.value,
      // }))
      jsFormInput.value = '';
      this.todoListModel.emit('change');
  }
  async render() {
    const fs_data = await collection.orderBy("created_at", "desc").get();
    const fs_dataItems = fs_data.docs.map( items => {
      return({
        dbId: items.id,
        id: items.data().id,
        title: items.data().title,
        isDone: items.data().isDone,
        created_at: items.data().created_at,
      })
    })
    this.todoListModel.setTodos(fs_dataItems);

    const jsTodoList = document.querySelector('#js-todo-list');
    const jsTodoCount = document.querySelector('#js-todo-count');
    const todos = this.todoListModel.getTodos()
    const totalCount = this.todoListModel.getTotalCount()
    jsTodoCount.textContent = `TodoItems: ${ totalCount }`

    const ul = element`<ul />`
    todos.forEach( todo => {
      const li = todo.isDone
        ? element`<li><input type="checkbox" class="checkbox" checked/><del>${ todo.title } : ${ todo.isDone } : ${ todo.id}</del><button class="delete">[x]</button></li>`
        : element`<li><input type="checkbox" class="checkbox" />${ todo.title } : ${ todo.isDone } : ${ todo.id}<button class="delete">[x]</button></li>`

      // チェックボックス状態
      const checkboxState = li.querySelector('.checkbox');
      checkboxState.addEventListener('change', async() => {
      //  const todos = this.todoListModel.getTodos();
      //  const item = todos.find( item => {
      //    return item.id === todo.id;
      //  })
      //  item.isDone = !item.isDone
        await collection.doc(todo.dbId).update({
          isDone: !todo.isDone
        })
       this.todoListModel.emit('change');
      })

      // 削除処理
      const deleteBtn = li.querySelector('.delete');
      deleteBtn.addEventListener('click', async() => {

      //  const todos = this.todoListModel.getTodos();
      //  const pos = todos.map( todo => {
      //    return todo.id
      //  }).indexOf(todo.id);
      //  todos.splice(pos, 1)

       await collection.doc(todo.dbId).delete();
       this.todoListModel.emit('change');
      })

      ul.appendChild(li)
    })
    render(ul,jsTodoList);
  }

  main() {
    const jsForm = document.querySelector('#js-form');
    const purge = document.querySelector('#purge');

    purge.addEventListener('click', () => {
      this.purge();
    })
    // changeリスナー関数
    this.todoListModel.addEventListener('change', () => {
      this.render();
    })  

    jsForm.addEventListener('submit', (event) => {
      event.preventDefault();
      this.inputTodo();
    });
    this.todoListModel.emit('change');
  }
}
```


Vue.js や React.js を使わないとどれだけ苦労するのかを、試してみましたが確かに記述は増えてhtmlの処理は面倒くさくなるものの。
書いているのは、一番楽しいという結果になりました。(笑)
生JS が一番練習になると思いますので、ツールを使わないとどうなるかの検証もいいかなと思います。

見てくれた方ありがとうございました。`m(_ _)m`

以上、`Docker　+` 終了です。


一応、`git hub` に UP しておきます。
https://github.com/atoris1192/docker-todos-vanilla