From b8781c54eba8f68e830471e0a80747afc31e14c2 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 12 Oct 2013 20:10:16 -0400 Subject: [PATCH] sd-model --- TODO.md | 1 - src/compiler.js | 1 + src/directives/index.js | 64 +++------ test/unit/specs/directives.js | 264 ++++++++++++++++++++++++---------- 4 files changed, 205 insertions(+), 125 deletions(-) diff --git a/TODO.md b/TODO.md index 95b1b8c72..c2c98f29f 100644 --- a/TODO.md +++ b/TODO.md @@ -1,4 +1,3 @@ -- tests for sd-model, remove sd-value and sd-checked - add escape: {{{ things in here should not be parsed }}} - sd-transition - component examples diff --git a/src/compiler.js b/src/compiler.js index 4fe770a47..661a2b1a6 100644 --- a/src/compiler.js +++ b/src/compiler.js @@ -537,6 +537,7 @@ CompilerProto.destroy = function () { utils.log('compiler destroyed: ', compiler.vm.$el) // unwatch compiler.observer.off() + compiler.emitter.off() var i, key, dir, inss, binding, el = compiler.el, directives = compiler.dirs, diff --git a/src/directives/index.js b/src/directives/index.js index aaddb752b..5bc3eee8c 100644 --- a/src/directives/index.js +++ b/src/directives/index.js @@ -8,15 +8,11 @@ module.exports = { }, text: function (value) { - this.el.textContent = isValidTextValue(value) - ? value - : '' + this.el.textContent = toText(value) }, html: function (value) { - this.el.innerHTML = isValidTextValue(value) - ? value - : '' + this.el.innerHTML = toText(value) }, style: { @@ -57,38 +53,6 @@ module.exports = { } }, - value: { - bind: function () { - var el = this.el, self = this - this.change = function () { - self.vm.$set(self.key, el.value) - } - el.addEventListener('keyup', this.change) - }, - update: function (value) { - this.el.value = value ? value : '' - }, - unbind: function () { - this.el.removeEventListener('keyup', this.change) - } - }, - - checked: { - bind: function () { - var el = this.el, self = this - this.change = function () { - self.vm.$set(self.key, el.checked) - } - el.addEventListener('change', this.change) - }, - update: function (value) { - this.el.checked = !!value - }, - unbind: function () { - this.el.removeEventListener('change', this.change) - } - }, - model: { bind: function () { var self = this, @@ -97,8 +61,8 @@ module.exports = { lazy = self.compiler.options.lazy self.event = (lazy || + el.tagName === 'SELECT' || type === 'checkbox' || - type === 'select' || type === 'radio') ? 'change' : 'keyup' @@ -111,11 +75,14 @@ module.exports = { el.addEventListener(self.event, self.set) }, update: function (value) { - this.el[this.attr] = this.attr === 'checked' - ? !!value - : isValidTextValue(value) - ? value - : '' + if (this.el.type === 'radio') { + /* jshint eqeqeq: false */ + this.el.checked = value == this.el.value + } else { + this.el[this.attr] = this.attr === 'checked' + ? !!value + : toText(value) + } }, unbind: function () { this.el.removeEventListener(this.event, this.set) @@ -167,6 +134,11 @@ function convertCSSProperty (prop) { }) } -function isValidTextValue (value) { - return typeof value === 'string' || typeof value === 'number' +/* + * Make sure only strings and numbers are output to html + */ +function toText (value) { + return (typeof value === 'string' || typeof value === 'number') + ? value + : '' } \ No newline at end of file diff --git a/test/unit/specs/directives.js b/test/unit/specs/directives.js index 01488de32..0cbf76ea6 100644 --- a/test/unit/specs/directives.js +++ b/test/unit/specs/directives.js @@ -213,89 +213,191 @@ describe('UNIT: Directives', function () { }) - describe('value', function () { + describe('model', function () { + + describe('input[checkbox]', function () { + + var dir = mockDirective('model', 'input', 'checkbox') + dir.bind() + + before(function () { + document.body.appendChild(dir.el) + }) + + it('should set checked on update()', function () { + dir.update(true) + assert.ok(dir.el.checked) + dir.update(false) + assert.ok(!dir.el.checked) + }) + + it('should trigger vm.$set when clicked', function () { + var triggered = false + dir.key = 'foo' + dir.vm = { $set: function (key, val) { + assert.strictEqual(key, 'foo') + assert.strictEqual(val, true) + triggered = true + }} + dir.el.dispatchEvent(mockMouseEvent('click')) + assert.ok(triggered) + }) + + it('should remove event listener with unbind()', function () { + var removed = true + dir.vm.$set = function () { + removed = false + } + dir.unbind() + dir.el.dispatchEvent(mockMouseEvent('click')) + assert.ok(removed) + }) + + after(function () { + document.body.removeChild(dir.el) + }) + + }) + + describe('input[radio]', function () { + + var dir1 = mockDirective('model', 'input', 'radio'), + dir2 = mockDirective('model', 'input', 'radio') + dir1.el.name = 'input-radio' + dir2.el.name = 'input-radio' + dir1.el.value = '12345' + dir2.el.value = '54321' + dir1.bind() + dir2.bind() + + before(function () { + document.body.appendChild(dir1.el) + document.body.appendChild(dir2.el) + }) + + it('should set el.checked on update()', function () { + assert.notOk(dir1.el.checked) + dir1.update(12345) + assert.ok(dir1.el.checked) + }) + + it('should trigger vm.$set when clicked', function () { + var triggered = false + dir2.key = 'radio' + dir2.vm = { $set: function (key, val) { + triggered = true + assert.strictEqual(key, 'radio') + assert.strictEqual(val, dir2.el.value) + }} + dir2.el.dispatchEvent(mockMouseEvent('click')) + assert.ok(triggered) + assert.ok(dir2.el.checked) + assert.notOk(dir1.el.checked) + }) + + it('should remove listeners on unbind()', function () { + var removed = true + dir1.vm = { $set: function () { + removed = false + }} + dir1.unbind() + dir1.el.dispatchEvent(mockMouseEvent('click')) + assert.ok(removed) + }) + + after(function () { + document.body.removeChild(dir1.el) + document.body.removeChild(dir2.el) + }) + + }) + + describe('select', function () { + + var dir = mockDirective('model', 'select') + dir.el.innerHTML = '' + dir.bind() + + before(function () { + document.body.appendChild(dir.el) + }) + + it('should set value on update()', function () { + dir.update(0) + assert.strictEqual(dir.el.value, '0') + }) + + it('should trigger vm.$set when value is changed', function () { + var triggered = false + dir.key = 'select' + dir.vm = { $set: function (key, val) { + triggered = true + assert.strictEqual(key, 'select') + assert.equal(val, 1) + }} + dir.el.options.selectedIndex = 1 + dir.el.dispatchEvent(mockChangeEvent()) + assert.ok(triggered) + }) + + it('should remove listener on unbind()', function () { + var removed = true + dir.vm = { $set: function () { + removed = false + }} + dir.unbind() + dir.el.dispatchEvent(mockChangeEvent()) + assert.ok(removed) + }) + + after(function () { + document.body.removeChild(dir.el) + }) + + }) - var dir = mockDirective('value', 'input') - dir.bind() - - before(function () { - document.body.appendChild(dir.el) - }) + describe('input[text] and others', function () { + + var dir = mockDirective('model', 'input', 'email') + dir.bind() + + before(function () { + document.body.appendChild(dir.el) + }) - it('should set the value on update()', function () { - dir.update('foobar') - assert.strictEqual(dir.el.value, 'foobar') - }) + it('should set the value on update()', function () { + dir.update('foobar') + assert.strictEqual(dir.el.value, 'foobar') + }) - it('should trigger vm.$set when value is changed via keyup', function () { - var triggered = false - dir.key = 'foo' - dir.vm = { $set: function (key, val) { - assert.strictEqual(key, 'foo') - assert.strictEqual(val, 'bar') - triggered = true - }} - dir.el.value = 'bar' - dir.el.dispatchEvent(mockKeyEvent('keyup')) - assert.ok(triggered) - }) + // `lazy` option is tested in the API suite + it('should trigger vm.$set when value is changed via keyup', function () { + var triggered = false + dir.key = 'foo' + dir.vm = { $set: function (key, val) { + assert.strictEqual(key, 'foo') + assert.strictEqual(val, 'bar') + triggered = true + }} + dir.el.value = 'bar' + dir.el.dispatchEvent(mockKeyEvent('keyup')) + assert.ok(triggered) + }) - it('should remove event listener with unbind()', function () { - var removed = true - dir.vm.$set = function () { - removed = false - } - dir.unbind() - dir.el.dispatchEvent(mockKeyEvent('keyup')) - assert.ok(removed) - }) + it('should remove event listener with unbind()', function () { + var removed = true + dir.vm.$set = function () { + removed = false + } + dir.unbind() + dir.el.dispatchEvent(mockKeyEvent('keyup')) + assert.ok(removed) + }) - after(function () { - document.body.removeChild(dir.el) - }) + after(function () { + document.body.removeChild(dir.el) + }) - }) - - describe('checked', function () { - - var dir = mockDirective('checked', 'input', 'checkbox') - dir.bind() - - before(function () { - document.body.appendChild(dir.el) - }) - - it('should set checked on update()', function () { - dir.update(true) - assert.ok(dir.el.checked) - dir.update(false) - assert.ok(!dir.el.checked) - }) - - it('should trigger vm.$set on change event', function () { - var triggered = false - dir.key = 'foo' - dir.vm = { $set: function (key, val) { - assert.strictEqual(key, 'foo') - assert.strictEqual(val, true) - triggered = true - }} - dir.el.dispatchEvent(mockMouseEvent('click')) - assert.ok(triggered) - }) - - it('should remove event listener with unbind()', function () { - var removed = true - dir.vm.$set = function () { - removed = false - } - dir.unbind() - dir.el.dispatchEvent(mockMouseEvent('click')) - assert.ok(removed) - }) - - after(function () { - document.body.removeChild(dir.el) }) }) @@ -441,7 +543,7 @@ function mockDirective (dirName, tag, type) { var dir = Seed.directive(dirName), ret = { binding: { compiler: { vm: {} } }, - compiler: { vm: {} }, + compiler: { vm: {}, options: {} }, el: document.createElement(tag || 'div') } if (typeof dir === 'function') { @@ -455,6 +557,12 @@ function mockDirective (dirName, tag, type) { return ret } +function mockChangeEvent () { + var e = document.createEvent('HTMLEvents') + e.initEvent('change', true, true) + return e +} + function mockKeyEvent (type) { var e = document.createEvent('KeyboardEvent'), initMethod = e.initKeyboardEvent