Vue.js 2.0 - Periodic Table solved

solving Periodic Table problem by separate logic from UI component

by Pasit R

HTML

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://unpkg.com/vue/dist/vue.js"></script>

<div id="app" class="container">
  <div class="row">
    <div class="col-xs-6">
      <h4>AddElement </h4>
      <div class="form-group">
        <label>Symbol:</label>
        <input class="form-control" v-model="adding.symbol" />
      </div>
      <div class="form-group">
        <label>Weight:</label>
        <input class="form-control" v-model="adding.weight" />
      </div>
      <div class="form-group">
        <label>Row:</label>
        <input class="form-control" v-model="adding.row" />
      </div>
      <div class="form-group">
        <label>Col:</label>
        <input class="form-control" v-model="adding.col" />
      </div>
      <button class="btn" v-on:click="addElement">AddElement</button>
    </div>
    <div class="col-xs-6">
      <h4>List of elements</h4>
      <ul>
        <li v-for="item in elements">{{item.symbol}}: {{item.atomicWeight}}</li>
      </ul>
    </div>
  </div>
  <div>
    <div class="col-xs-6">
      <h4>QueryElement (row, col)</h4>
      <input v-model="query.row" placeholder="Row" />
      <input v-model="query.col" placeholder="Col" />
      <button v-on:click="queryElement" class="btn">Query</button>
    </div>
    <div class="col-xs-6">
      <h4>CalculateMolarWeight (formula)</h4>
      <input type="text" v-model="formula" placeholder="eg. H2O" />
      <button v-on:click="calculateMolarWeight" class="btn">Calculate</button>
    </div>
    <div class="col-xs-12">
      <h4>Result:</h4>
      <pre>{{ message }}</pre>
    </div>
  </div>
</div>

JavaScript

// Model
class PeriodicTable {
  constructor() {
    this.items = []
    this.formulaPattern = /[A-Z](:?[a-z0-9]*)/g
  }

  addElement(symbol, atomicWeight, col, row) {
    const el = {
      symbol: symbol.trim(),
      atomicWeight: parseFloat(atomicWeight),
      col: parseInt(col, 10),
      row: parseInt(row, 10)
    }
    if (!this.isValidSymbol(el.symbol)) {
      throw new Error(`invalid symbol ${symbol}`)
    }
    if (!el.atomicWeight || el.atomicWeight < 0) {
      throw new Error(`invalid weight ${atomicWeight}`)
    }
    const exists = this.findSymbol(el.symbol)
    if (exists) {
      throw new Error(`${el.symbol} already exists`)
    }
    const found = this.queryElement(el.row, el.col)
    if (found) {
      throw new Error(`${el.row}, ${el.col} already exists`)
    }
    this.items.push(el)
  }

  queryElement(row, col) {
    let colNum = parseInt(col)
    let rowNum = parseInt(row)
    let match = (item) => item.col === colNum && item.row === rowNum
    var result = this.items.filter(match)
    if (result.length === 0) {
      return null
    }
    return result[0].symbol
  }

  calculateMolarWeight(input) {
    const items = this.extractFormula(input)
    const sum = (prev, current) => {
      let weight = current.weight * current.quantity
      return prev + weight
    }
    const total = items
      .map(item => this.extractSymbol(item))
      .reduce(sum, 0)
      // eliminate binary floating point
    return parseFloat(total.toPrecision(10))
  }

  extractFormula(formula) {
    const result = []
    let match
    while (match = this.formulaPattern.exec(formula)) {
      result.push(match[0])
    }
    return result
  }

  extractSymbol(formula) {
    const pattern = /([A-Z][a-z]*)([0-9]*)/g
    let match = pattern.exec(formula)
    let symbol = match[1]
    let quantity = match[2] ? parseInt(match[2], 10) : 1
    let weight = this.getAtomicWeight(symbol)
    return {
      symbol,
      quantity,
      weight
    }
  }

 ...