JSFiddle - React, Tailwind, and code Playground

HTML

Select your side:
    <select onchange="updateText('side', event)">
      <option value="---">---</option>
      <option value="Good">Good</option>
      <option value="Evil">Evil</option>
      <option value="Noop">noop</option>
    </select>

    <fieldset>
      The good one:
      <p s-text="goodCharacter"></p>
      <input type="text" s-model="goodCharacter" oninput="updateText('goodCharacter', event)"/>
    </fieldset>
    <fieldset>
      The evil one:
      <p s-text="evilCharacter"></p>
      <input type="text" s-model="evilCharacter" oninput="updateText('evilCharacter', event)"/>
    </fieldset>

    <h3>Your character:</h3>
    <p s-text="selectedCharacter"></p>
    <label>Sentence length:</label>
    <strong s-text="selectedCharacterSentenceLength"></strong>

JavaScript

function Seer (config) {
  let signals = {}
  let Dep = {
    // Name of the currently evaluated computed value
    // Doesn’t get overriden even if it depends on other computed values
    target: null,
    // Stores dependency keys of computed values
    subs: {},
    depend (deps, dep) {
      // Add the computed value as depending on this value
      // if not yet added
      if (!deps.includes(this.target)) {
        deps.push(this.target)
      }
      // Add this value as a dependency of the computed value
      // if not yet added
      if (!Dep.subs[this.target].includes(dep)) {
        Dep.subs[this.target].push(dep)
      }
    },
    getValidDeps (deps, key) {
      // Filter only valid dependencies by removing dead dependencies
      // that were not used during last computation
      return deps.filter(dep => this.subs[dep].includes(key))
    },
    notifyDeps (deps) {
      // notify all existing deps
      deps.forEach(notify)
    }
  }

  observeData(config.data)
  subscribeWatchers(config.watch, config.data)

  return {
    data: config.data,
    observe,
    notify
  }

  function subscribeWatchers(watchers, context) {
    for (let key in watchers) {
      if (watchers.hasOwnProperty(key)) {
        observe(key, watchers[key].bind(context))
      }
    }
  }

  function observe (property, signalHandler) {
    if(!signals[property]) signals[property] = []

    signals[property].push(signalHandler)
  }

  function notify (signal) {
    if(!signals[signal] || signals[signal].length < 1) return

    signals[signal].forEach(signalHandler => signalHandler())
  }

  function makeReactive (obj, key, computeFunc) {
    let deps = []
    let val = obj[key]

    Object.defineProperty(obj, key, {
      get () {
        // Run only when getting within a computed value context
        if (Dep.target) {
          Dep.depend(deps, key)
        }

        return val
      },
      set (newVal) {
        val = newVal

        // Clean up and notify valid deps
   ...