1. 仕様

  • 機能仕様

    • 3で割り切れる場合は「Fizz」を出力する。

    • 5で割り切れる場合は「Buzz」を出力する。

    • 両者で割り切れる場合は「FizzBuzz」を出力する。

    • 上記の条件以外の場合は番号を返す。

    • 指定された回数だけ繰り返し実行する。

    • タイプごとに出力を切り替えることができる。

      • タイプ1は通常、タイプ2は数字のみ、タイプ3は FizzBuzzの場合のみをプリントする。

  • 画面仕様

    • 1から100までを1件ずつ画面に表示できる。

    • 1から100まで表示された内容を編集して保存できる。

    • 編集して保存した内容を確認して削除できる。

2. 設計

2.1. TODOリスト

  • ✓ 繰り返し実行できるようにする

  • ✓ 「Fizz」を出力できるようにする

  • ✓ 「Buzz」を出力できるようにする

  • ✓ 「FizzBuzz」を出力できるようにする

  • ✓ タイプ 1 の場合

    • ✓ 3の倍数のときは数の代わりに「Fizz」をプリントできるようにする。

    • ✓ 5 の倍数のときは「Buzz」とプリントできるようにする。

    • ✓ 3 と 5両方の倍数の場合には「FizzBuzz」とプリントできるようにする。

  • ✓ タイプ 2 の場合

  • ✓ 3 の倍数のときは数をプリントできるようにする。

    • ✓ 5 の倍数のときは数をプリントできるようにする。

    • ✓ 3 と 5 両方の倍数の場合には値をプリントできるようにする。

  • ✓ タイプ 3 の場合

    • ✓ 3 の倍数のときは数をプリントできるようにする。

    • ✓ 5 の倍数のときは数をプリントできるようにする。

    • ✓ 3 と 5両方の倍数の場合には「FizzBuzz」とプリントできるようにする。

  • ✓ カウンター画面

    • ✓ 画面を作る。

    • ✓ インクリメントできるようにする。

    • ✓ デクリメントできるようにする。

  • ❏ 一覧編集画面

    • ✓ 画面を作る。

    • ✓ 編集できるようにする。

    • ✓ 保存できるようにする。

    • ✓ 保存した内容を表示できるようにする。

    • ✓ 保存した内容を削除できるようにする。

2.2. ユースケース図

2.3. クラス図

2.3.1. パッケージ構成

diag 11279637fb6396289ecd3d41e91ab17b

2.3.2. Presentaion

View
diag c3585223afe5ec4e9bacbf079174107d

2.3.3. Application

Service
diag 8b1dedf0202659352f89d8a0a3e22f17
Repository
diag 042aa24a313fec78f7204a2632a9fcd8

2.3.4. Infrastructure

diag eb7488ec5fb49536d4e9399a5527dfa7

2.3.5. Domain

Model
diag 6598f93039ac03da51a1e16d25b412c9
Type
diag 7a3e1d63f15ba51dd5e457d0bb802358

2.4. シーケンス図

3. 開発

Infrastracture

/* eslint-disable prefer-promise-reject-errors */
/* eslint-disable no-unused-vars */
export default class IndexedDbRepository {
  constructor (dbName, storeName) {
    this._dbName = dbName
    this._storeName = storeName
  }

  createRequest (dbName, storeName) {
    const openReq = indexedDB.open(dbName)

    openReq.onupgradeneeded = function (event) {
      const db = event.target.result
      const os = db.createObjectStore(storeName, { keyPath: 'id' })
    }

    return openReq
  }

  put (data) {
    return new Promise((resolve, reject) => {
      const req = this.createRequest(this._dbName, this._storeName)
      req.onsuccess = event => {
        const db = event.target.result
        const trans = db.transaction(this._storeName, 'readwrite')
        const store = trans.objectStore(this._storeName)

        try {
          const putReq = store.put(data)

          putReq.onsuccess = () => {
            console.log('put data success')
            resolve()
          }

          req.onerror = event => {
            console.log('put data fails')
          }

          trans.oncomplete = () => {
            console.log('transactoin complete')
          }
        } catch (error) {
          console.log(error)
          reject(error.message)
        }
      }
    })
  }

  add (aList) {
    return new Promise((resolve, reject) => {
      const req = this.createRequest(this._dbName, this._storeName)
      req.onsuccess = event => {
        const db = event.target.result
        const trans = db.transaction(this._storeName, 'readwrite')
        trans.oncomplete = () => {
          console.log('transaction complete')
          resolve()
        }

        trans.onerror = event => {
          console.log(event)
          reject()
        }

        const store = trans.objectStore(this._storeName)
        aList.forEach(data => {
          try {
            const addReq = store.add(data)
            addReq.onsuccess = () => {
              console.log('add data success')
            }
            addReq.onerror = event => {
              console.log(`add data fails ${event.target.error}`)
            }
          } catch (error) {
            console.log(error)
            reject(error.message)
          }
        })
      }
    })
  }

  get (keyValue) {
    return new Promise((resolve, reject) => {
      const req = this.createRequest(this._dbName, this._storeName)
      req.onsuccess = event => {
        const db = event.target.result
        const trans = db.transaction(this._storeName, 'readwrite')
        const store = trans.objectStore(this._storeName)

        try {
          const getReq = store.get(keyValue)

          getReq.onsuccess = event => {
            const result = event.target.result
            console.log(result)
            if (result === undefined) {
              reject(`id:${keyValue} not found`)
            } else {
              resolve(result)
            }
          }
        } catch (error) {
          console.log(error)
          reject(error.message)
        }
      }
    })
  }

  openCursor () {
    return new Promise((resolve, reject) => {
      const req = this.createRequest(this._dbName, this._storeName)
      req.onsuccess = event => {
        const db = event.target.result
        const trans = db.transaction(this._storeName, 'readwrite')
        const store = trans.objectStore(this._storeName)

        try {
          const getReq = store.openCursor()
          const result = []
          getReq.onsuccess = event => {
            const cur = event.target.result
            if (!cur) return resolve(result)
            result.push(cur.value)
            cur.continue()
          }
          getReq.onerror = event => {
            console.log(`selectAll fails ${event.target.error}`)
          }
        } catch (error) {
          console.log(error)
          reject(error.message)
        }
      }
    })
  }

  delete (keyValue) {
    return new Promise((resolve, reject) => {
      const req = this.createRequest(this._dbName, this._storeName)
      req.onsuccess = event => {
        const db = event.target.result
        const trans = db.transaction(this._storeName, 'readwrite')
        const store = trans.objectStore(this._storeName)

        try {
          const deleteReq = store.delete(keyValue)

          deleteReq.onsuccess = event => {
            console.log('delete success')
            resolve()
          }

          deleteReq.onerror = event => {
            console.log(`delete fails ${event.target.error}`)
          }
        } catch (error) {
          console.log(error)
          reject(error.message)
        }
      }
    })
  }

  deleteDatabase () {
    return new Promise((resolve, reject) => {
      const deleteReq = indexedDB.deleteDatabase(this._dbName)
      deleteReq.onsuccess = () => {
        console.log('db destroy success')
        resolve()
      }

      deleteReq.onerror = event => {
        console.log(`db delete fail ${event.target.error}`)
      }
    })
  }
}

3.1. Application

3.1.1. Repository

import IndexedDbRepository from '../../infrastructure/IndexedDbRepository'
import FizzBuzzEntity from '../../domain/model/fizz-buzz/FizzBuzzEntity'

export default class FizzBuzzRepository extends IndexedDbRepository {
  // eslint-disable-next-line no-useless-constructor
  constructor (dbName, storeName) {
    super(dbName, storeName)
  }

  save (data) {
    return super.put({ id: data.id, list: data.list })
  }

  saveBatch (entities) {
    const aList = entities.map(entity => {
      return { id: entity.id, list: entity.list }
    })
    return super.add(aList)
  }

  find (keyValue) {
    return super.get(keyValue).then(data => {
      return new Promise((resolve, reject) => {
        const entity = new FizzBuzzEntity(data.list, data.id)
        resolve(entity)
      })
    })
  }

  selectAll () {
    return super.openCursor().then(aList => {
      return new Promise((resolve, reject) => {
        const entities = aList.map(
          data => new FizzBuzzEntity(data.list, data.id)
        )
        resolve(entities)
      })
    })
  }

  delete (keyValue) {
    return super.delete(keyValue)
  }

  destroy () {
    return super.deleteDatabase()
  }
}

3.1.2. Service

/* eslint-disable no-useless-constructor */
export default class FizzBuzzCommand {
  constructor () {}
  execute () {}
}
import FizzBuzzCommand from './FizzBuzzCommand'

export default class FizzBuzzValueCommand extends FizzBuzzCommand {
  constructor (type) {
    super()
    this._type = type
  }

  execute (number) {
    return this._type.generate(number).value
  }
}
import FizzBuzzCommand from './FizzBuzzCommand'
import FizzBuzzList from '../../../domain/model/fizz-buzz/FizzBuzzList'

export default class FizzBuzzListCommand extends FizzBuzzCommand {
  constructor (type) {
    super()
    this._type = type
    this._list = new FizzBuzzList([])
  }

  execute (number) {
    // 配列は0から始まるので1を足す
    [...Array(number + 1).keys()]
      .slice(1)
      .forEach(i => (this._list = this._list.add(this._type.generate(i))))
    return this._list.value
  }
}
import FizzBuzzValueCommand from './FizzBuzzValueCommand'
import FizzBuzzListCommand from './FizzBuzzListCommand'
import FizzBuzzEntity from '../../../domain/model/fizz-buzz/FizzBuzzEntity'
import FizzBuzzRepository from '../../repository/FizzBuzzRepository'
import FizzBuzzTypeEnum from '../../../domain/type/fizz-buzz/FizzBuzzTypeEnum'
import FizzBuzzList from '../../../domain/model/fizz-buzz/FizzBuzzList'

export default class FizzBuzzService {
  constructor (type) {
    this._valueCommand = new FizzBuzzValueCommand(type)
    this._listCommand = new FizzBuzzListCommand(type)
    this._repository = new FizzBuzzRepository('fizzbuzz.db', 'items')
  }

  static get Type01 () {
    return FizzBuzzTypeEnum.Type01
  }

  static get Type02 () {
    return FizzBuzzTypeEnum.Type02
  }

  static get Type03 () {
    return FizzBuzzTypeEnum.Type03
  }

  static get MAX_COUNT () {
    return FizzBuzzList.MAX_COUNT
  }

  static valueOf (value) {
    return FizzBuzzTypeEnum.valuOf(value)
  }

  generate (number) {
    return this._valueCommand.execute(number)
  }

  generateList (number) {
    return this._listCommand.execute(number)
  }

  save (list) {
    const record = new FizzBuzzEntity(list)
    return this._repository.save(record)
  }

  selectAll () {
    return this._repository.selectAll()
  }

  delete (id) {
    return this._repository.delete(id)
  }

  deleteAll () {
    return this._repository.destroy()
  }
}

3.2. Domain

3.2.1. Model

export default class FizzBuzzEntity {
  constructor (list, id) {
    this._list = list
    this._id = id || FizzBuzzEntity.generateUuid()
  }

  static generateUuid () {
    let uuid = ''
    for (let i = 0; i < 32; i++) {
      const random = (Math.random() * 16) | 0

      if (i === 8 || i === 12 || i === 16 || i === 20) {
        uuid += '-'
      }
      uuid += (i === 12 ? 4 : i === 16 ? (random & 3) | 8 : random).toString(
        16
      )
    }
    return uuid
  }

  get id () {
    return this._id
  }

  get list () {
    return this._list
  }
}
export default class FizzBuzzList {
  constructor (list) {
    // eslint-disable-next-line curly
    if (list.length > FizzBuzzList.MAX_COUNT)
      throw new Error(
        `FizzBuzzList can't generate over ${FizzBuzzList.MAX_COUNT} items`
      )

    this._value = list
  }

  static get MAX_COUNT () {
    return 100
  }

  get value () {
    return this._value
  }

  toString () {
    return this._value
  }

  add (value) {
    const result = this._value
    result.push(value)
    return new FizzBuzzList(result)
  }
}
export default class FizzBuzzValue {
  constructor (number, value) {
    // eslint-disable-next-line curly
    if (number < 0)
      throw new Error(
        `FizzBuzzValue can't generate by minus nnumber ${number}`
      )

    this._number = number
    this._value = value
  }

  get value () {
    return this._value
  }

  get number () {
    return this._number
  }

  toString () {
    return this._value
  }

  equals (other) {
    return this._number === other._number && this._value === other._value
  }
}

3.2.2. Type

export default class FizzBuzzType {
  constructor () {
    this.FIZZ = 'Fizz'
    this.BUZZ = 'Buzz'
  }

  generate (number) {}

  isFizz (number) {
    return number % 3 === 0
  }

  isBuzz (number) {
    return number % 5 === 0
  }
}
import FizzBuzzType from './FizzBuzzType'
import FizzBuzzValue from '../../model/fizz-buzz/FizzBuzzValue'

export default class FizzBuzzType01 extends FizzBuzzType {
  // eslint-disable-next-line no-useless-constructor
  constructor () {
    super()
  }

  generate (number) {
    const isFizz = this.isFizz(number)
    const isBuzz = this.isBuzz(number)

    if (isFizz && isBuzz) return new FizzBuzzValue(number, this.FIZZ + this.BUZZ)
    if (isFizz) return new FizzBuzzValue(number, this.FIZZ)
    if (isBuzz) return new FizzBuzzValue(number, this.BUZZ)
    return new FizzBuzzValue(number, number)
  }
}
import FizzBuzzType from './FizzBuzzType'
import FizzBuzzValue from '../../model/fizz-buzz/FizzBuzzValue'

export default class FizzBuzzType02 extends FizzBuzzType {
  // eslint-disable-next-line no-useless-constructor
  constructor () {
    super()
  }

  generate (number) {
    return new FizzBuzzValue(number, number)
  }
}
import FizzBuzzType from './FizzBuzzType'
import FizzBuzzValue from '../../model/fizz-buzz/FizzBuzzValue'

export default class FizzBuzzType03 extends FizzBuzzType {
  // eslint-disable-next-line no-useless-constructor
  constructor () {
    super()
  }

  generate (number) {
    const isFizz = this.isFizz(number)
    const isBuzz = this.isBuzz(number)

    if (isFizz && isBuzz) return new FizzBuzzValue(number, this.FIZZ + this.BUZZ)
    return new FizzBuzzValue(number, number)
  }
}
import FizzBuzzType01 from './FizzBuzzType01'
import FizzBuzzType02 from './FizzBuzzType02'
import FizzBuzzType03 from './FizzBuzzType03'

export default class FizzBuzzTypeEnum {
  static get Type01 () {
    return new FizzBuzzType01()
  }

  static get Type02 () {
    return new FizzBuzzType02()
  }

  static get Type03 () {
    return new FizzBuzzType03()
  }

  static valuOf (value) {
    switch (value) {
      case 'one':
        return FizzBuzzTypeEnum.Type01
      case 'two':
        return FizzBuzzTypeEnum.Type02
      case 'three':
        return FizzBuzzTypeEnum.Type03
      default:
        throw Error(`${value} is not defined enum type`)
    }
  }
}

3.3. Presentation

3.3.1. View

export default class Button {
  // eslint-disable-next-line no-useless-constructor
  constructor () {}

  create (id, tableId, label) {
    return `
            <button
              id="${id}"
              type="button"
              class="btn btn-primary btn-rounded btn-sm my-0"
              data-tableid="${tableId}"
            >
              ${label}
            </button>
          `
  }
}
export default class Message {
  // eslint-disable-next-line no-useless-constructor
  constructor () {}

  static get WARNING () {
    return 1
  }

  static get SUCCESS () {
    return 2
  }

  static get DANGER () {
    return 3
  }

  static get selectorId () {
    return 'app__message'
  }

  static create (message, type) {
    switch (type) {
      case 1:
        return `
                    <div class="alert alert-warning alert-dismissible fade show" role="alert">
                        ${message}
                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
              `
      case 2:
        return `
                    <div class="alert alert-success alert-dismissible fade show" role="alert">
                        ${message}
                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
              `
      case 3:
        return `
                    <div class="alert alert-danger alert-dismissible fade show" role="alert">
                        ${message}
                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
              `
      default:
        return `
                    <div class="alert alert-primary alert-dismissible fade show" role="alert">
                        ${message}
                        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                            <span aria-hidden="true">&times;</span>
                        </button>
                    </div>
              `
    }
  }

  clear () {
    document.querySelector(`#${Message.selectorId}`).innerHTML = ''
  }

  render (message, type) {
    document.querySelector(`#${Message.selectorId}`).innerHTML = Message.create(
      message,
      type
    )
  }
}
export default class Table {
  // eslint-disable-next-line no-useless-constructor
  constructor () {}

  create (aList) {
    const header = (() => {
      return [...Array(10).keys()].map(i => `<th>${i + 1}</th>`)
    })()

    const body = (() => {
      const result = []
      let row = 0
      let col = []
      aList.forEach((v, k) => {
        if ((k + 1) % 10 === 0) {
          col.push(`<td>${v}</td>`)
          result[row++] = col
          col = []
        } else {
          col.push(`<td>${v}</td>`)
        }
      })
      return result
    })()

    const row = n => body[n].join(' ')

    const result = `
            <table>
              <thead>
                ${header[0]}
                ${header[1]}
                ${header[2]}
                ${header[3]}
                ${header[4]}
                ${header[5]}
                ${header[6]}
                ${header[7]}
                ${header[8]}
                ${header[9]}
              </thead>
              <tbody>
                <tr>${row(0)}</tr>
                <tr>${row(1)}</tr>
                <tr>${row(2)}</tr>
                <tr>${row(3)}</tr>
                <tr>${row(4)}</tr>
                <tr>${row(5)}</tr>
                <tr>${row(6)}</tr>
                <tr>${row(7)}</tr>
                <tr>${row(8)}</tr>
                <tr>${row(9)}</tr>
              </tbody>
            </table>
        `
    return result
  }
}
export default class Counter {
  constructor (service) {
    this._counter = 0
    this._service = service
    this._value = this._service.generate(this._counter)
  }

  incrementEvent () {
    this._counter += 1
    this._value = this._service.generate(this._counter)
    this.render(this._selector)
  }

  decrementEvent () {
    this._counter === 0 ? (this._counter = 0) : (this._counter -= 1)
    this._value = this._service.generate(this._counter)
    this.render(this._selector)
  }

  renderComponent () {
    const renderMainComponent = () => {
      const incrementId = `${this._selector.appCounterId}__increment`
      const decrementId = `${this._selector.appCounterId}__decrement`

      const dispatchEvent = () => {
        document
          .querySelector(`#${incrementId}`)
          .addEventListener('click', this.incrementEvent.bind(this))
        document
          .querySelector(`#${decrementId}`)
          .addEventListener('click', this.decrementEvent.bind(this))
      }

      const renderCounter = () => {
        const createCounter = (incrementId, decrementId) => {
          return `
                  <div class="col-md-1">
                    <button class="btn btn-primary" id="${decrementId}">
                      -
                    </button>
                  </div>
                  <div class="col-md-10">
                    <h1 class="display-1 text-center">${this._value}</h1>
                  </div>
                  <div class="col-md-1">
                    <button class="btn btn-primary" id="${incrementId}">
                      +
                    </button>
                  </div>
                `
        }

        document.querySelector(
          `#${this._selector.appCounterId}`
        ).innerHTML = createCounter(incrementId, decrementId)
      }

      // eslint-disable-next-line no-unused-vars
      const createMainComponent = (events => {
        renderCounter()

        events()
      })(dispatchEvent)
    }

    // eslint-disable-next-line no-unused-vars
    const render = (() => {
      renderMainComponent()
    })()
  }

  render (selector) {
    this._selector = selector
    this.renderComponent()
  }
}
/* eslint-disable no-undef */
import Table from './Table'
import Button from './Button'
import Message from './Message'
import FizzBuzzService from '../../../application/service/fizz-buzz/FizzBuzzService'

export default class TableCreateUpdate {
  constructor (service) {
    this._table = new Table()
    this._button = new Button()
    this._message = new Message()
    this._service = service
    this._list = this._service.generateList(FizzBuzzService.MAX_COUNT)
  }

  changeEvent (e) {
    this._service = new FizzBuzzService(
      FizzBuzzService.valueOf(e.target.value)
    )
    this._list = this._service.generateList(FizzBuzzService.MAX_COUNT)
    this._selected = e.target.value
    this.render(this._selector)
  }

  saveTableEvent (e) {
    const createListFromTable = table => {
      const list = []
      for (let i = 1; i <= 10; i++) {
        for (let j = 0; j < 10; j++) {
          list.push(table.rows[i].cells[j].innerText)
        }
      }
      return list
    }
    const table = document.querySelector(
      `#${e.target.dataset.tableid} > table`
    )
    const result = createListFromTable(table)
    this._service
      .save(result)
      .then(() => {
        this._message.render('保存しました', Message.SUCCESS)
        this.render(this._selector)
      })
      .catch(error => {
        this._message.render(error, Message.DANGER)
      })
  }

  renderComponent () {
    const renderMainComponent = () => {
      const renderSelectId = `${this._selector.appCreateUpdateId}__select`
      const renderSelectTypeId = `${renderSelectId}--type`
      const renderTableId = `${this._selector.appCreateUpdateId}__table`
      const renderButtonTableId = `${renderTableId}__button`
      const renderButtonTableSaveId = `${renderButtonTableId}--save`

      const dispatchEvent = () => {
        document
          .querySelector(`#${renderSelectTypeId}`)
          .addEventListener('change', this.changeEvent.bind(this))
        document
          .querySelector(`#${renderButtonTableSaveId}`)
          .addEventListener('click', this.saveTableEvent.bind(this))
      }

      const renderSelect = selected => {
        const createSelect = id => {
          switch (selected) {
            case 'two':
              return `
                        <select name="type" id="${id}">
                          <option value="one">タイプ1</option>
                          <option selected value="two">タイプ2</option>
                          <option value="three">タイプ3</option>
                        </select>
                        `
            case 'three':
              return `
                        <select name="type" id="${id}">
                          <option value="one">タイプ1</option>
                          <option value="two">タイプ2</option>
                          <option selected value="three">タイプ3</option>
                        </select>
                        `
            default:
              return `
                        <select name="type" id="${id}">
                          <option value="one">タイプ1</option>
                          <option value="two">タイプ2</option>
                          <option value="three">タイプ3</option>
                        </select>
                        `
          }
        }

        document.querySelector(`#${renderSelectId}`).innerHTML = createSelect(
          renderSelectTypeId
        )

        document
          .querySelector(`#${renderSelectTypeId}`)
          .classList.add('browser-default')
        document
          .querySelector(`#${renderSelectTypeId}`)
          .classList.add('custom-select')
      }

      const renderTable = aList => {
        const createTable = aList => {
          return this._table.create(aList)
        }

        const editTable = () => {
          $(`#${renderTableId} > table > tbody > tr > td`).on(
            'click',
            editToggle(this._service)
          )

          function save (value) {
            document.querySelector(
              `#${Message.selectorId}`
            ).innerHTML = Message.create(`${value}に編集しました。`)
          }

          function editToggle (service) {
            let editFlag = false
            return function () {
              if (editFlag) return

              const $input = $('<input>')
                .attr('type', 'text')
                .val($(this).text())

              $(this).html($input)

              $('input', this)
                .focus()
                .keypress(function (e) {
                  if (e.which === 13) {
                    const number = $(this).val()
                    try {
                      const value = service.generate(number)
                      save(value)
                      $(this)
                        .after(value)
                        .unbind()
                        .remove()
                      editFlag = false
                    } catch (e) {
                      document.querySelector(
                        `#${Message.selectorId}`
                      ).innerHTML = Message.create(e.message, Message.DANGER)
                    }
                  }
                })

              $('input', this)
                .focus()
                .blur(function (e) {
                  $(this)
                    .after($(this).val())
                    .unbind()
                    .remove()
                  editFlag = false
                })
              editFlag = true
            }
          }
        }

        document.querySelector(`#${renderTableId}`).innerHTML = createTable(
          aList
        )
        document
          .querySelector(`#${renderTableId} > table`)
          .classList.add('table')
        document
          .querySelector(`#${renderTableId} > table`)
          .classList.add('table-striped')
        editTable()
      }

      const renderButtonSave = () => {
        const createButton = (id, renderTableId, label) => {
          return this._button.create(id, renderTableId, label)
        }

        document.querySelector(
          `#${renderButtonTableId}`
        ).innerHTML = createButton(
          renderButtonTableSaveId,
          renderTableId,
          '保存'
        )
      }

      // eslint-disable-next-line no-unused-vars
      const createMainComponent = (events => {
        document.querySelector(
          `#${this._selector.appCreateUpdateId}`
        ).innerHTML = `
                <div id="${renderSelectId}"></div>
                <div id="${renderTableId}"></div>
                <div id="${renderButtonTableId}"></div>
              `

        renderSelect(this._selected)
        renderTable(this._list)
        renderButtonSave()

        events()
      })(dispatchEvent)
    }

    // eslint-disable-next-line no-unused-vars
    const render = (() => {
      renderMainComponent()
    })()
  }

  render (selector) {
    this._selector = selector
    this.renderComponent()
  }
}
import Table from './Table'
import Button from './Button'
import Message from './Message'

export default class TableReadDelete {
  constructor (service) {
    this._table = new Table()
    this._button = new Button()
    this._message = new Message()
    this._service = service
  }

  changeReadEvent (e) {
    const record = this._record.filter(data => data.id === e.target.value)
    this._selected = e.target.value
    if (record[0] !== undefined) this.render(this._selector)
  }

  deleteTableEvent (e) {
    const id = document.querySelector(
      `#${this._selector.appReadDeleteId}__select__select-read`
    ).value
    this._service.delete(id).then(() => {
      this._message.render(`${id}を削除しました。`, Message.SUCCESS)
      this.render(this._selector)
    })
  }

  deleteAllEvent (e) {
    this._service.deleteAll().then(() => {
      this._message.render('全てのレコードを削除しました。', Message.SUCCESS)
      this.render(this._selector)
    })
  }

  renderComponent () {
    const renderMainComponent = () => {
      const renderSelectReadId = `${this._selector.appReadDeleteId}__select`
      const renderSelectReadSelectId = `${renderSelectReadId}__select-read`
      const renderTableReadId = `${this._selector.appReadDeleteId}__table`
      const renderButtonDeleteId = `${this._selector.appReadDeleteId}__button`
      const renderButtonDeleteSelectId = `${renderButtonDeleteId}--delete-select`
      const renderButtonDeleteAllId = `${renderButtonDeleteId}--delete-all`

      const dispatchEvent = () => {
        document
          .querySelector(`#${renderSelectReadSelectId}`)
          .addEventListener('change', this.changeReadEvent.bind(this))

        document
          .querySelector(`#${renderButtonDeleteSelectId}`)
          .addEventListener('click', this.deleteTableEvent.bind(this))

        document
          .querySelector(`#${renderButtonDeleteAllId}`)
          .addEventListener('click', this.deleteAllEvent.bind(this))
      }

      const renderSelectRead = data => {
        const createSelectRead = (id, aList) => {
          const options = []
          aList.forEach(data => {
            if (this._selected === data.id) {
              options.push(`<option selected>${data.id}</option>`)
            } else {
              options.push(`<option>${data.id}</option>`)
            }
          })
          return `
                  <select name="type" id="${id}">
                    ${options.join(' ')}
                  </select>
                `
        }

        document.querySelector(
          `#${renderSelectReadId}`
        ).innerHTML = createSelectRead(renderSelectReadSelectId, data)

        document
          .querySelector(`#${renderSelectReadSelectId}`)
          .classList.add('browser-default')
        document
          .querySelector(`#${renderSelectReadSelectId}`)
          .classList.add('custom-select')
      }

      const renderTableRead = aList => {
        const createTable = aList => {
          return this._table.create(aList)
        }

        document.querySelector(`#${renderTableReadId}`).innerHTML = createTable(
          aList
        )

        document
          .querySelector(`#${renderTableReadId} > table`)
          .classList.add('table')
        document
          .querySelector(`#${renderTableReadId} > table`)
          .classList.add('table-striped')
      }

      const renderButtonDelete = () => {
        const createButton = (id, tableId, label) => {
          return this._button.create(id, tableId, label)
        }

        let button = createButton(
          renderButtonDeleteSelectId,
          renderTableReadId,
          '削除'
        )
        button += createButton(
          renderButtonDeleteAllId,
          renderTableReadId,
          '全削除'
        )
        document.querySelector(`#${renderButtonDeleteId}`).innerHTML = button
      }

      // eslint-disable-next-line no-unused-vars
      const createMainComponent = (events => {
        document.querySelector(
          `#${this._selector.appReadDeleteId}`
        ).innerHTML = `
                <div id="${renderSelectReadId}"></div>
                <div id="${renderTableReadId}"></div>
                <div id="${renderButtonDeleteId}"></div>
              `

        this._service.selectAll().then(data => {
          renderSelectRead(data)
          if (data[0] !== undefined) {
            if (this._selected !== undefined) {
              renderTableRead(
                data.filter(v => v.id === this._selected)[0].list
              )
            } else {
              renderTableRead(data[0].list)
            }
          }
          renderButtonDelete()
          this._record = data

          events()
        })
      })(dispatchEvent)
    }

    // eslint-disable-next-line no-unused-vars
    const render = (() => {
      renderMainComponent()
    })()
  }

  render (selector) {
    this._selector = selector
    this.renderComponent()
  }
}
import FizzBuzzService from '../../../application/service/fizz-buzz/FizzBuzzService'
import Message from './Message'
import Counter from './Counter'
import TableCreateUpdate from './TableCreateUpdate'
import TableReadDelete from './TableReadDelete'

export default class FizzBuzzView {
  constructor (type) {
    this._type = type || FizzBuzzService.Type01
    this._message = new Message()
    this._service = new FizzBuzzService(this._type)
    this._counterComponent = new Counter(this._service)
    this._tableCreateUpdateComponent = new TableCreateUpdate(this._service)
    this._tableReadDeleteComponent = new TableReadDelete(this._service)
  }

  counterEvent (e) {
    this._counterComponent.render(this._selector)
    this._message.clear()
  }

  createUpdateEvent (e) {
    this._tableCreateUpdateComponent.render(this._selector)
    this._message.clear()
  }

  readDeleteEvent (e) {
    this._tableReadDeleteComponent.render(this._selector)
    this._message.clear()
  }

  renderComponent () {
    const selector = {
      appId: 'app',
      msgId: Message.selectorId,
      appCounterId: 'fizz-buzz-app-counter',
      appCreateUpdateId: 'fizz-buzz-app-create-update',
      appReadDeleteId: 'fizz-buzz-app-read-delete',
      counterTabId: 'tab-menu01',
      createUpdateTabId: 'tab-menu02',
      readDeleteTabId: 'tab-menu03',
      counterPanelId: 'panel-menu01',
      createUpdatePanelId: 'panel-menu02',
      readDeletePanelId: 'panel-menu03'
    }

    const renderMainComponent = () => {
      const dispatchEvent = () => {
        document
          .querySelector(`#${selector.counterTabId}`)
          .addEventListener('click', this.counterEvent.bind(this))
        document
          .querySelector(`#${selector.createUpdateTabId}`)
          .addEventListener('click', this.createUpdateEvent.bind(this))
        document
          .querySelector(`#${selector.readDeleteTabId}`)
          .addEventListener('click', this.readDeleteEvent.bind(this))
      }

      // eslint-disable-next-line no-unused-vars
      const createMainComponent = (events => {
        document.querySelector(`#${selector.appId}`).innerHTML = `
                <div class="py-3">
                  <section id="menu">
                    <div class="container">
                      <h3 id="function-name" class="mb-3">FizzBuzz</h3>
                      <div id="${selector.msgId}"></div>
                      <div class="nav nav-tabs" id="tab-menus" role="tablist">
                        <a
                          aria-controls="${selector.counterPanelId}"
                          aria-selected="true"
                          class="nav-item nav-link active"
                          data-toggle="tab"
                          href="#${selector.counterPanelId}"
                          id="${selector.counterTabId}"
                          role="tab"
                          >Counter</a
                        >
                        <a
                          aria-controls="${selector.createUpdatePanelId}"
                          aria-selected="false"
                          class="nav-item nav-link"
                          data-toggle="tab"
                          href="#${selector.createUpdatePanelId}"
                          id="${selector.createUpdateTabId}"
                          role="tab"
                          >Table(Create&Update)</a
                        >
                        <a
                          aria-controls="${selector.readDeletePanelId}"
                          aria-selected="false"
                          class="nav-item nav-link"
                          data-toggle="tab"
                          href="#${selector.readDeletePanelId}"
                          id="${selector.readDeleteTabId}"
                          role="tab"
                          >Table(Read&Delete)</a
                        >
                      </div>

                      <div class="tab-content" id="panel-menus">
                        <div
                          aria-labelledby="${selector.counterTabId}"
                          class="tab-pane fade show active border border-top-0 jumbotron"
                          id="${selector.counterPanelId}"
                          role="tabpanel"
                        >
                          <div
                            id="${selector.appCounterId}"
                            class="row d-flex align-items-center"
                          ></div>
                        </div>
                        <div
                          aria-labelledby="${selector.createUpdateTabId}"
                          class="tab-pane fade border border-top-0"
                          id="${selector.createUpdatePanelId}"
                          role="tabpanel"
                        >
                          <dvi class="row p-3">
                            <div
                              id="${selector.appCreateUpdateId}"
                              class="col-md-12 order-md-2"
                            ></div>
                          </dvi>
                        </div>
                        <div
                          aria-labelledby="${selector.readDeleteTabId}"
                          class="tab-pane fade border border-top-0"
                          id="${selector.readDeletePanelId}"
                          role="tabpanel"
                        >
                          <dvi class="row p-3">
                            <div
                              id="${selector.appReadDeleteId}"
                              class="col-md-12 order-md-2"
                            ></div>
                          </dvi>
                        </div>
                      </div>
                    </div>
                  </section>
                </div>
            `

        events()
      })(dispatchEvent)
    }

    const renderSubComponent = () => {
      this._counterComponent.render(selector)
      this._tableCreateUpdateComponent.render(selector)
      this._tableReadDeleteComponent.render(selector)
      this._selector = selector
    }

    // eslint-disable-next-line no-unused-vars
    const render = (() => {
      renderMainComponent()
      renderSubComponent()
    })()
  }

  render () {
    this.renderComponent()
  }
}

4. 参照