diff --git a/Gruntfile.js b/Gruntfile.js index 33ade139e..5d5ad013b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -65,12 +65,9 @@ module.exports = function( grunt ) { }, watch: { - options: { - livereload: true - }, - component: { + dev: { files: ['src/**/*.js', 'component.json'], - tasks: ['component_build'] + tasks: ['component_build', 'jsc'] } } diff --git a/src/compiler.js b/src/compiler.js index d58341a1f..a629432b8 100644 --- a/src/compiler.js +++ b/src/compiler.js @@ -556,31 +556,42 @@ CompilerProto.getOption = function (type, id) { * Unbind and remove element */ CompilerProto.destroy = function () { - var compiler = this - log('compiler destroyed: ', compiler.vm.$el) - // unwatch - compiler.observer.off() - compiler.emitter.off() - var i, key, dir, inss, binding, + + var compiler = this, + i, key, dir, instances, binding, el = compiler.el, directives = compiler.dirs, exps = compiler.exps, - bindings = compiler.bindings - // remove all directives that are instances of external bindings + bindings = compiler.bindings, + teardown = compiler.options.teardown + + // call user teardown first + if (teardown) teardown() + + // unwatch + compiler.observer.off() + compiler.emitter.off() + + // unbind all direcitves i = directives.length while (i--) { dir = directives[i] + // if this directive is an instance of an external binding + // e.g. a directive that refers to a variable on the parent VM + // we need to remove it from that binding's instances if (!dir.isSimple && dir.binding.compiler !== compiler) { - inss = dir.binding.instances - if (inss) inss.splice(inss.indexOf(dir), 1) + instances = dir.binding.instances + if (instances) instances.splice(instances.indexOf(dir), 1) } dir.unbind() } + // unbind all expressions (anonymous bindings) i = exps.length while (i--) { exps[i].unbind() } + // unbind/unobserve all own bindings for (key in bindings) { if (hasOwn.call(bindings, key)) { @@ -591,6 +602,7 @@ CompilerProto.destroy = function () { binding.unbind() } } + // remove self from parentCompiler var parent = compiler.parentCompiler, childId = compiler.childId @@ -600,7 +612,8 @@ CompilerProto.destroy = function () { delete parent.vm.$[childId] } } - // remove el + + // finally remove dom element if (el === document.body) { el.innerHTML = '' } else if (el.parentNode) { diff --git a/test/unit/specs/api.js b/test/unit/specs/api.js index 362487a95..17bd16f61 100644 --- a/test/unit/specs/api.js +++ b/test/unit/specs/api.js @@ -500,6 +500,22 @@ describe('UNIT: API', function () { }) + describe('teardown', function () { + + it('should be called when a vm is destroyed', function () { + var called = false + var Test = Seed.extend({ + teardown: function () { + called = true + } + }) + var test = new Test() + test.$destroy() + assert.ok(called) + }) + + }) + describe('transitions', function () { // it('should be tested', function () { // assert.ok(false) diff --git a/test/unit/specs/viewmodel.js b/test/unit/specs/viewmodel.js index 142ad945f..612d5d499 100644 --- a/test/unit/specs/viewmodel.js +++ b/test/unit/specs/viewmodel.js @@ -1,11 +1,3 @@ -/* - * Only tests the following: - * - .$get() - * - .$set() - * - .$watch() - * - .$unwatch() - */ - describe('UNIT: ViewModel', function () { mock('vm-test', '{{a.b.c}}') @@ -227,4 +219,144 @@ describe('UNIT: ViewModel', function () { }) + describe('.$destroy', function () { + + // since this simply delegates to Compiler.prototype.destroy(), + // that's what we are actually testing here. + var destroy = require('seed/src/compiler').prototype.destroy + + var tearDownCalled = false, + observerOffCalled = false, + emitterOffCalled = false, + dirUnbindCalled = false, + expUnbindCalled = false, + bindingUnbindCalled = false, + unobserveCalled = 0, + elRemoved = false, + externalBindingUnbindCalled = false + + var dirMock = { + binding: { + compiler: null, + instances: [] + }, + unbind: function () { + dirUnbindCalled = true + } + } + dirMock.binding.instances.push(dirMock) + + var bindingsMock = Object.create({ + 'test2': { + unbind: function () { + externalBindingUnbindCalled = true + } + } + }) + bindingsMock.test = { + root: true, + key: 'test', + value: { + __observer__: { + off: function () { + unobserveCalled++ + return this + } + } + }, + unbind: function () { + bindingUnbindCalled = true + } + } + + var compilerMock = { + options: { + teardown: function () { + tearDownCalled = true + } + }, + observer: { + off: function () { + observerOffCalled = true + }, + proxies: { + 'test.': {} + } + }, + emitter: { + off: function () { + emitterOffCalled = true + } + }, + dirs: [dirMock], + exps: [{ + unbind: function () { + expUnbindCalled = true + } + }], + bindings: bindingsMock, + childId: 'test', + parentCompiler: { + childCompilers: [], + vm: { + $: { + 'test': true + } + } + }, + el: { + parentNode: { + removeChild: function () { + elRemoved = true + } + } + } + } + + compilerMock.parentCompiler.childCompilers.push(compilerMock) + + destroy.call(compilerMock) + + it('should call the teardown option', function () { + assert.ok(tearDownCalled) + }) + + it('should turn observer and emitter off', function () { + assert.ok(observerOffCalled) + assert.ok(emitterOffCalled) + }) + + it('should unbind all directives', function () { + assert.ok(dirUnbindCalled) + }) + + it('should remove directives from external bindings', function () { + assert.strictEqual(dirMock.binding.instances.indexOf(dirMock), -1) + }) + + it('should unbind all expressions', function () { + assert.ok(expUnbindCalled) + }) + + it('should unbind and unobserve own bindings', function () { + assert.ok(bindingUnbindCalled) + assert.strictEqual(unobserveCalled, 3) + }) + + it('should not unbind external bindings', function () { + assert.notOk(externalBindingUnbindCalled) + }) + + it('should remove self from parentCompiler', function () { + var parent = compilerMock.parentCompiler + assert.ok(parent.childCompilers.indexOf(compilerMock), -1) + assert.strictEqual(parent.vm.$[compilerMock.childId], undefined) + }) + + it('should remove the dom element', function () { + assert.ok(elRemoved) + }) + + }) + }) \ No newline at end of file