<div class="container">
  <div class="table-responsive">
    <table class="table table-striped table-bordered table-hover">
      <thead>
        <th>
          Input One
        </th>
        <th>
          Operation
        </th>
        <th>
          Input Two
        </th>
        <th>
          Sum
        </th>
      </thead>
      <tbody>
        <tr class="table-row" id="row0">
          <td contenteditable class="input1"></td>
          <td>
            <div class="btn-group btn-group-toggle w-100">
              <label class="btn btn-secondary active">
                <input type="radio" name="operation" checked>+
              </label>
              <label class="btn btn-secondary">
                <input type="radio" name="operation">−
              </label>
              <label class="btn btn-secondary">
                <input type="radio" name="operation">×
              </label>
              <label class="btn btn-secondary">
                <input type="radio" name="operation">÷
              </label>
            </div>
          </td>
          <td contenteditable class="input2"></td>
          <td></td>
        </tr>
      </tbody>
    </table>
  </div>
  <button class="btn btn-primary btn-large btn-block" id="add-row">Add Row</button>
</div>
td {
  width: 25%;
}
// clear CodePen console
console.clear();

// query DOM
const table = document.getElementsByClassName('table')[0];
const tbody = document.getElementsByTagName('tbody')[0];
const addRow = document.getElementById('add-row');

// define depend and notify methods
class Dep {
  constructor() {
    // use Set to mitigate duplicates
    this.subscribers = new Set();
  }
  
  depend() {
    if (activeUpdate) {
      // register current activeUpdate as a subscriber
      this.subscribers.add(activeUpdate);
    }
  }
  
  notify() {
    // run all subscriber functions
    // invoked in insertion order
    this.subscribers.forEach(sub => sub());
  }
};

// define the addition of getters and setters to obj argument (state)
const observe = (obj) => {
  Object.keys(obj).forEach(key => {
    let stateValue = obj[key];
    const dep = new Dep();
    
    Object.defineProperty(obj, key, {
      get() {       
        dep.depend();
        return stateValue;
      },
      set(newValue) {
        const hasChanged = stateValue !== newValue;
        if (hasChanged) {
          stateValue = newValue;
          dep.notify();
        }
      }
    });
    
  });
};

// when updateOnStateChange invokes and in turn invokes a state getter,
// then the wrappedUpdate function is added to its subscriber Set
let activeUpdate = null;
const updateOnStateChange = update => {
  function wrappedUpdate() {
    activeUpdate = wrappedUpdate;
    update();
    activeUpdate = null;
  }
  
  wrappedUpdate();
};

/* START FIRST ROW STATE AND DEPENDENCIES */

// initiate state
const state = {
  row0: {
    input1: '',
    operation: '+',
    input2: '',
    total: ''
  }
};

// observe first row
observe(state.row0);

// update DOM
updateOnStateChange(() => {
  const row0total = document.getElementById('row0').lastElementChild;
  row0total.innerText = state.row0.total;
})

// update state
updateOnStateChange(() => {
  if ( (state.row0.input1 === '' || state.row0.input2 === '') || (state.row0.input2 === 0 && state.row0.operation === '÷') ) {
    state.row0.total = '';
  } else {
    // ensure operations are rounded to two decimal places
    switch (state.row0.operation) {
      case '+':
        state.row0.total = Math.round( (state.row0.input1 + state.row0.input2) * 100 ) / 100;
        break;
      case '−':
        state.row0.total = Math.round( (state.row0.input1 - state.row0.input2) * 100 ) / 100;
        break;
      case '×':
        state.row0.total = Math.round( (state.row0.input1 * state.row0.input2) * 100 ) / 100; 
        break;
      case '÷':
        state.row0.total = Math.round( (state.row0.input1 / state.row0.input2) * 100 ) / 100;
        break;
      default:
        console.log('default switch statement');
    }
  }
});

/* END FIRST ROW DEPENDENCIES */
/* START EVENT LISTENERS */

// delegate Enter key to table
table.addEventListener('keydown', e => {
  if (e.key === 'Enter') {
    e.target.blur();
  }
});

// delegate row operations to table
table.addEventListener('click', e => {
  if (e.target.tagName === 'LABEL') {
    const row = e.target.closest('.table-row');
    
    // update state
    state[row.id].operation = e.target.innerText;
    
    // update DOM
    row.querySelector('.active').classList.remove('active');
    e.target.classList.add('active');
    e.target.firstElementChild.checked = true;
  }
});

// helper
const parse = str => {
  const num = parseFloat(str);
  if (isNaN(num) || str === 'Infinity') return '';
  else return num;
};

// add event listener to input one and input two
tbody.addEventListener('input', e => {
  const row = e.target.parentElement.id;
  
  if (e.target.classList.contains('input1')) {
    state[row].input1 = parse(e.target.innerText);
  }
  
  if (e.target.classList.contains('input2')) {
    state[row].input2 = parse(e.target.innerText);
  }
});

// button to add another row
addRow.addEventListener('click', e => {
  // get row number
  const rowNumber = tbody.children.length;
  
  // append row
  tbody.innerHTML += `
    <tr class="table-row" id="row${rowNumber}">
      <td contenteditable class="input1"></td>
      <td>
        <div class="btn-group btn-group-toggle w-100">
          <label class="btn btn-secondary active">
            <input type="radio" name="operation" checked>+
          </label>
          <label class="btn btn-secondary">
            <input type="radio" name="operation">−
          </label>
          <label class="btn btn-secondary">
            <input type="radio" name="operation">×
          </label>
          <label class="btn btn-secondary">
            <input type="radio" name="operation">÷
          </label>
        </div>
      </td>
      <td contenteditable class="input2"></td>
      <td></td>
    </tr>
  `;
  
  state[`row${rowNumber}`] = {
    input1: '',
    operation: '+',
    input2: '',
    total: ''
  };
  
  // observe new row state
  observe(state[`row${rowNumber}`]);

  // update DOM
  updateOnStateChange(() => {
    const newRowTotal = document.getElementById(`row${rowNumber}`).lastElementChild;
    newRowTotal.innerText = state[`row${rowNumber}`].total;
  });
 
  // update state
  updateOnStateChange(() => {
    if ( (state[`row${rowNumber}`].input1 === '' || state[`row${rowNumber}`].input2 === '') || (state[`row${rowNumber}`].input2 === 0 && state[`row${rowNumber}`].operation === '÷') ) {
      state[`row${rowNumber}`].total = '';
    } else {
      // ensure operations are rounded to two decimal places
      switch (state[`row${rowNumber}`].operation) {
        case '+':
          state[`row${rowNumber}`].total = Math.round( (state[`row${rowNumber}`].input1 + state[`row${rowNumber}`].input2) * 100 ) / 100;
          break;
        case '−':
          state[`row${rowNumber}`].total = Math.round( (state[`row${rowNumber}`].input1 - state[`row${rowNumber}`].input2) * 100 ) / 100;
          break;
        case '×':
          state[`row${rowNumber}`].total = Math.round( (state[`row${rowNumber}`].input1 * state[`row${rowNumber}`].input2) * 100 ) / 100;
          break;
        case '÷':
          state[`row${rowNumber}`].total = Math.round( (state[`row${rowNumber}`].input1 / state[`row${rowNumber}`].input2) * 100 ) / 100;
          break;
        default:
          console.log('default switch statement');
      }
    }
  });
});

/* END EVENT LISTENERS */

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap.min.css

External JavaScript

This Pen doesn't use any external JavaScript resources.