<p class="js-bound-quote">My favorite {{ movie }} quote is "{{ quote }}".</p>  
class BoundNode {
  constructor (node) {
    this.template = node.innerHTML
    this.node = node
  }
  
  update (data) {
    let temp_template = this.template.slice(0)
    this.node.innerHTML = temp_template.replace(/\{\{\s?(\w+)\s?\}\}/g, (match, variable) => {
    return data[variable] || ''
    })
  }
}

class BoundModel {
  constructor (handlers) {
    const callbacks = []
    const data = {
      add_callback: function add_callback (fn) {
        callbacks.push(fn)
      }
    }

    const proxy = new Proxy(data, {
      set: function (target, property, value) {
        target[property] = value
        callbacks.forEach((callback) => callback())
        return true
      }
    })
    
    return proxy 
  }
}




const quotes = [
  "What is the point of owning a race car if you can't drive it?",
  "Give me a scotch, I'm starving.",
  "I'm a huge fan of the way you lose control and turn into an enourmous green rage monster.",
  "I already told you, I don't want to join your super secret boy band.",
  "You know, it's times like these when I realize what a superhero I am."
]

const my_node = new BoundNode(document.querySelector('.js-bound-quote'))
const my_model = new BoundModel()

my_model.add_callback(function () {
  my_node.update(my_model)
})

my_model.movie = 'Iron Man 2'
window.setInterval(function () {
  my_model.quote = quotes[Math.floor(Math.random() * quotes.length)]
}, 1000)




describe('BoundNode', () => {
  let test_node, bound_test_node
  let test_node_text = 'My favorite movie is "{{ movie }}".'
  
  beforeEach(() => {
    test_node = document.createElement('div')
    test_node.innerHTML = test_node_text
    bound_test_node = new BoundNode(test_node)
  })
  
  afterEach(() => {
    test_node = null
    bound_test_node = null
  })
  
  it('remembers the original text as the template', () => {
    expect(bound_test_node.template).toBe(test_node_text)
  })
  
  it('can update the innerHTML', () => {
    const expected_text = 'My favorite movie is "Mrs. Doubtfire".'
    bound_test_node.update({
      movie: 'Mrs. Doubtfire'
    })
    expect(bound_test_node.node.innerHTML).toBe(expected_text)
  })
  
  it('can update the innerHTML multiple times', () => {
    const expected_text_1 = 'My favorite movie is "Mrs. Doubtfire".'
    const expected_text_2 = 'My favorite movie is "The Godfather".'
    bound_test_node.update({
      movie: 'Mrs. Doubtfire'
    })
    expect(bound_test_node.node.innerHTML).toBe(expected_text_1)
    
    bound_test_node.update({
      movie: 'The Godfather'
    })
    expect(bound_test_node.node.innerHTML).toBe(expected_text_2)
  })
})

describe('BoundModel', () => {
  let test_model
  
  beforeEach(() => {
    test_model = new BoundModel()
  })
  
  afterEach(() => {
    test_model = null
  })
  
  it('returns a Proxy instance', () => {
    expect(Object.getPrototypeOf(my_model)).toEqual({})
  })
  
  it('allows setting arbitrary values', () => {
    my_model.foo = 'bar'
    expect(my_model.foo).toBe('bar')
  })
  
  it('can accept a callback for setting values', () => {
    let has_been_called = false
    my_model.add_callback(() => {
      has_been_called = true
    })
    my_model.foo = 'bar'
    
    expect(has_been_called).toBe(true)
  })
  
  it('executes multiple callbacks on #set calls', () => {
    let calls = 0
    my_model.add_callback(function callback_1 () {
      calls++
    })
    my_model.add_callback(function callback_2 () {
      calls++
    })
    expect(calls).toBe(0)
    my_model.foo = 'bar'
    expect(calls).toBe(2)
  })
})

describe('Binding a node to a model', () => {
  it('shows that a model can be bound to a node in a sane way', () => {
    const quote_1 = "What is the point of owning a race car if you can't drive it?"
    const quote_2 = "Give me a scotch, I'm starving."
    
    const test_node = document.createElement('div')
    test_node.innerHTML = '<h1>{{ the_title }}</h1>'
    const test_bound_node = new BoundNode(test_node)
    
    const test_model = new BoundModel()

    test_model.add_callback(function () {
      test_bound_node.update(test_model)
    })

    test_model.the_title = 'Kung Fu Hustle'
    expect(test_node.innerHTML).toBe('<h1>Kung Fu Hustle</h1>')
    
    test_model.the_title = 'The Spy Who Loved Me'
    expect(test_node.innerHTML).toBe('<h1>The Spy Who Loved Me</h1>')
  })
})
View Compiled

External CSS

  1. https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.4/jasmine.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.4/jasmine.js
  2. https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.4/jasmine-html.js
  3. https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.4/boot.js