Browse Source

v-if refactor + change to use childVM

Evan You 12 years ago
parent
commit
3df6343a81

+ 6 - 2
src/compiler.js

@@ -27,7 +27,7 @@ var Emitter     = require('./emitter'),
     // list of priority directives
     // that needs to be checked in specific order
     priorityDirectives = [
-        //'if',
+        'if',
         'repeat',
         'view',
         'component'
@@ -368,7 +368,7 @@ CompilerProto.compile = function (node, root) {
  */
 CompilerProto.checkPriorityDir = function (dirname, node, root) {
     var expression, directive, Ctor
-    if (dirname === 'component' && root !== true && (Ctor = this.resolveComponent(node, undefined, true))) {
+    if (dirname === 'component' && (Ctor = this.resolveComponent(node, undefined, true))) {
         directive = Directive.parse(dirname, '', this, node)
         directive.Ctor = Ctor
     } else {
@@ -376,6 +376,10 @@ CompilerProto.checkPriorityDir = function (dirname, node, root) {
         directive = expression && Directive.parse(dirname, expression, this, node)
     }
     if (directive) {
+        if (root === true) {
+            utils.warn('Directive v-' + dirname + ' cannot be used on manually instantiated root node.')
+            return
+        }
         this.deferred.push(directive)
         return true
     }

+ 28 - 50
src/directives/if.js

@@ -1,69 +1,47 @@
-var config = require('../config'),
-    transition = require('../transition')
+var utils    = require('../utils')
 
 module.exports = {
 
     bind: function () {
-        this.parent = this.el.parentNode || this.el.vue_if_parent
-        this.ref = document.createComment(config.prefix + '-if-' + this.key)
-        var detachedRef = this.el.vue_if_ref
-        if (detachedRef) {
-            this.parent.insertBefore(this.ref, detachedRef)
+        
+        this.parent = this.el.parentNode
+        this.ref    = document.createComment('vue-if')
+        this.Ctor   = this.compiler.resolveComponent(this.el)
+
+        // insert ref
+        this.parent.insertBefore(this.ref, this.el)
+        this.parent.removeChild(this.el)
+
+        if (utils.attr(this.el, 'view')) {
+            utils.warn('Conflict: v-if cannot be used together with v-view')
+        }
+        if (utils.attr(this.el, 'repeat')) {
+            utils.warn('Conflict: v-if cannot be used together with v-repeat')
         }
-        this.el.vue_if_ref = this.ref
     },
 
     update: function (value) {
 
-        var el = this.el
-
-        // sometimes we need to create a VM on a detached node,
-        // e.g. in v-repeat. In that case, store the desired v-if
-        // state on the node itself so we can deal with it elsewhere.
-        el.vue_if = !!value
-
-        var parent   = this.parent,
-            ref      = this.ref,
-            compiler = this.compiler
-
-        if (!parent) {
-            if (!el.parentNode) {
-                return
-            } else {
-                parent = this.parent = el.parentNode
-            }
-        }
-
         if (!value) {
-            transition(el, -1, remove, compiler)
-        } else {
-            transition(el, 1, insert, compiler)
-        }
-
-        function remove () {
-            if (!el.parentNode) return
-            // insert the reference node
-            var next = el.nextSibling
-            if (next) {
-                parent.insertBefore(ref, next)
+            this._unbind()
+        } else if (!this.childVM) {
+            this.childVM = new this.Ctor({
+                el: this.el.cloneNode(true),
+                parent: this.vm
+            })
+            if (this.compiler.init) {
+                this.parent.insertBefore(this.childVM.$el, this.ref)
             } else {
-                parent.appendChild(ref)
+                this.childVM.$before(this.ref)
             }
-            parent.removeChild(el)
-        }
-
-        function insert () {
-            if (el.parentNode) return
-            parent.insertBefore(el, ref)
-            parent.removeChild(ref)
         }
+        
     },
 
     unbind: function () {
-        this.el.vue_if_ref = this.el.vue_if_parent = null
-        var ref = this.ref
-        if (ref.parentNode) {
-            ref.parentNode.removeChild(ref)
+        if (this.childVM) {
+            this.childVM.$destroy()
+            this.childVM = null
         }
     }
 }

+ 3 - 3
src/directives/index.js

@@ -18,15 +18,15 @@ module.exports = {
         isLiteral: true,
         bind: function () {
             if (!this.el.vue_vm) {
-                this.component = new this.Ctor({
+                this.childVM = new this.Ctor({
                     el: this.el,
                     parent: this.vm
                 })
             }
         },
         unbind: function () {
-            if (this.component) {
-                this.component.$destroy()
+            if (this.childVM) {
+                this.childVM.$destroy()
             }
         }
     },

+ 11 - 25
src/directives/repeat.js

@@ -200,12 +200,6 @@ module.exports = {
         var ref = vms.length > index
             ? vms[index].$el
             : self.ref
-        
-        // if reference VM is detached by v-if,
-        // use its v-if ref node instead
-        if (!ref.parentNode) {
-            ref = ref.vue_if_ref
-        }
 
         // check if data already exists in the old array
         oldIndex = self.oldVMs ? indexOf(self.oldVMs, data) : -1
@@ -225,10 +219,6 @@ module.exports = {
 
             // first clone the template node
             el = self.el.cloneNode(true)
-            // then we provide the parentNode for v-if
-            // so that it can still work in a detached state
-            el.vue_if_parent = ctn
-            el.vue_if_ref = ref
 
             // we have an alias, wrap the data
             if (self.arg) {
@@ -277,22 +267,18 @@ module.exports = {
         // Finally, DOM operations...
         el = item.$el
         if (existing) {
-            // we simplify need to re-insert the existing node
-            // to its new position. However, it can possibly be
-            // detached by v-if. in that case we insert its v-if
-            // ref node instead.
-            ctn.insertBefore(el.parentNode ? el : el.vue_if_ref, ref)
+            // existing vm, we simplify need to re-insert
+            // its element to the new position.
+            ctn.insertBefore(el, ref)
         } else {
-            if (el.vue_if !== false) {
-                if (self.compiler.init) {
-                    // do not transition on initial compile,
-                    // just manually insert.
-                    ctn.insertBefore(el, ref)
-                    item.$compiler.execHook('attached')
-                } else {
-                    // give it some nice transition.
-                    item.$before(ref)
-                }
+            if (self.compiler.init) {
+                // do not transition on initial compile,
+                // just manually insert.
+                ctn.insertBefore(el, ref)
+                item.$compiler.execHook('attached')
+            } else {
+                // give it some nice transition.
+                item.$before(ref)
             }
         }
     },

+ 1 - 3
src/directives/view.js

@@ -21,9 +21,7 @@ module.exports = {
 
     update: function(value) {
 
-        if (this.childVM) {
-            this.childVM.$destroy()
-        }
+        this._unbind()
 
         var Ctor  = this.compiler.getOption('components', value)
         if (!Ctor) return

+ 1 - 8
test/functional/fixtures/transition.html

@@ -34,14 +34,6 @@
         <button class="pop" v-on="click:pop">pop</button>
         <button class="splice" v-on="click:splice">splice</button>
     </div>
-    <div
-        class="test"
-        v-repeat="items"
-        v-if="filter(this)"
-        v-transition
-        v-attr="data-id:a">
-        {{a}}
-    </div>
     <div
         class="test"
         v-repeat="items"
@@ -50,6 +42,7 @@
         v-attr="data-id:a">
         {{a}}
     </div>
+    <div class="test if" v-transition v-if="items.length > 1">This is only visible when item.length > 1</div>
 </div>
 <h1 style="margin:0">123</h1>
 

+ 10 - 14
test/functional/specs/transition.js

@@ -1,4 +1,4 @@
-casper.test.begin('CSS Transition', 25, function (test) {
+casper.test.begin('CSS Transition', 20, function (test) {
 
     var minWait = 50,
         transDuration = 200
@@ -6,59 +6,55 @@ casper.test.begin('CSS Transition', 25, function (test) {
     casper
     .start('./fixtures/transition.html', function () {
         test.assertElementCount('.test', 3)
+        test.assertVisible('.test.if')
         test.assertNotVisible('.test[data-id="1"]')
     })
     .thenClick('.button-0')
     .wait(minWait, function () {
-        test.assertElementCount('.test', 4)
         test.assertVisible('.test[data-id="1"]')
     })
     .thenClick('.button-1')
     .wait(minWait, function () {
-        test.assertElementCount('.test', 4)
-        test.assertElementCount('.test.v-leave', 2)
+        test.assertElementCount('.test.v-leave', 1)
     })
     .wait(transDuration, function () {
-        test.assertElementCount('.test', 3)
         test.assertElementCount('.test.v-leave', 0)
         test.assertNotVisible('.test[data-id="1"]')
     })
     .thenClick('.button-2')
     .wait(minWait, function () {
-        test.assertElementCount('.test', 3)
-        test.assertElementCount('.test.v-leave', 2)
+        test.assertElementCount('.test.v-leave', 1)
     })
     .wait(transDuration, function () {
-        test.assertElementCount('.test', 2)
+        test.assertElementCount('.test.v-leave', 0)
         test.assertNotVisible('.test[data-id="1"]')
         test.assertNotVisible('.test[data-id="2"]')
     })
     .thenClick('.push')
     .wait(minWait, function () {
-        test.assertElementCount('.test', 4)
         test.assertVisible('.test[data-id="3"]')
     })
     .thenClick('.pop')
+    .thenClick('.pop')
     .wait(minWait, function () {
-        test.assertElementCount('.test', 4)
         test.assertElementCount('.test.v-leave', 2)
     })
     .wait(transDuration, function () {
-        test.assertElementCount('.test', 2)
+        test.assertNotVisible('.test.if')
         test.assertNotVisible('.test[data-id="1"]')
         test.assertNotVisible('.test[data-id="2"]')
     })
     .thenClick('.splice')
     .wait(minWait, function () {
-        test.assertElementCount('.test', 3)
         test.assertVisible('.test[data-id="99"]')
     })
     // test Array swapping with transition
     .thenEvaluate(function () {
         test.items = [test.items[1], {a:3}]
     })
-    .wait(transDuration + minWait, function () {
-        test.assertElementCount('.test', 3)
+    .wait(minWait, function () {
+        test.assertVisible('.test.if')
+        test.assertVisible('.test[data-id="99"]')
         test.assertVisible('.test[data-id="3"]')
     })
     .run(function () {

+ 148 - 151
test/unit/specs/directives.js

@@ -413,62 +413,6 @@ describe('Directives', function () {
 
     })
 
-    describe('if', function () {
-
-        it('should remain detached if it was detached during bind() and never attached', function () {
-            var dir = mockDirective('if')
-            dir.bind()
-            dir.update(true)
-            assert.notOk(dir.el.parentNode)
-            dir.update(false)
-            assert.notOk(dir.el.parentNode)
-        })
-
-        it('should remove el and insert ref when value is falsy', function () {
-            var dir = mockDirective('if'),
-                parent = document.createElement('div')
-            parent.appendChild(dir.el)
-            dir.bind()
-            dir.update(false)
-            assert.notOk(dir.el.parentNode)
-            assert.notOk(parent.contains(dir.el))
-            // phantomJS weird bug:
-            // Node.contains() returns false when argument is a comment node.
-            assert.strictEqual(dir.ref.parentNode, parent)
-        })
-
-        it('should append el and remove ref when value is truthy', function () {
-            var dir = mockDirective('if'),
-                parent = document.createElement('div')
-            parent.appendChild(dir.el)
-            dir.bind()
-            dir.update(false)
-            dir.update(true)
-            assert.strictEqual(dir.el.parentNode, parent)
-            assert.ok(parent.contains(dir.el))
-            assert.notOk(parent.contains(dir.ref))
-        })
-
-        it('should work even if it was detached during bind()', function () {
-            var dir = mockDirective('if')
-            dir.bind()
-            var parent = document.createElement('div')
-            parent.appendChild(dir.el)
-
-            dir.update(false)
-            assert.strictEqual(dir.parent, parent)
-            assert.notOk(dir.el.parentNode)
-            assert.notOk(parent.contains(dir.el))
-            assert.strictEqual(dir.ref.parentNode, parent)
-
-            dir.update(true)
-            assert.strictEqual(dir.el.parentNode, parent)
-            assert.ok(parent.contains(dir.el))
-            assert.notOk(parent.contains(dir.ref))
-        })
-
-    })
-
     describe('on', function () {
         
         var dir = mockDirective('on')
@@ -743,6 +687,154 @@ describe('Directives', function () {
 
     })
 
+    describe('style', function () {
+        
+        it('should apply a normal style', function () {
+            var d = mockDirective('style')
+            d.arg = 'text-align'
+            d.bind()
+            assert.strictEqual(d.prop, 'textAlign')
+            d.update('center')
+            assert.strictEqual(d.el.style.textAlign, 'center')
+        })
+
+        it('should apply prefixed style', function () {
+            var d = mockDirective('style')
+            d.arg = '-webkit-transform'
+            d.bind()
+            assert.strictEqual(d.prop, 'webkitTransform')
+            d.update('scale(2)')
+            assert.strictEqual(d.el.style.webkitTransform, 'scale(2)')
+        })
+
+        it('should auto prefix styles', function () {
+            var d = mockDirective('style')
+            d.arg = '$transform'
+            d.bind()
+            assert.ok(d.prefixed)
+            assert.strictEqual(d.prop, 'transform')
+            var val = 'scale(2)'
+            d.update(val)
+            assert.strictEqual(d.el.style.transform, val)
+            assert.strictEqual(d.el.style.webkitTransform, val)
+            assert.strictEqual(d.el.style.mozTransform, val)
+            assert.strictEqual(d.el.style.msTransform, val)
+        })
+
+        it('should set cssText if no arg', function () {
+            var d = mockDirective('style')
+            d.bind()
+            var val = 'color:#fff'
+            d.update(val)
+            assert.strictEqual(d.el.style.color, 'rgb(255, 255, 255)')
+        })
+
+    })
+
+    describe('cloak', function () {
+        
+        it('should remove itself after the instance is ready', function () {
+            // it doesn't make sense to test with a mock for this one, so...
+            var v = new Vue({
+                template: '<div v-cloak></div>',
+                replace: true,
+                ready: function () {
+                    // this hook is attached before the v-cloak hook
+                    // so it should still have the attribute
+                    assert.ok(this.$el.hasAttribute('v-cloak'))
+                }
+            })
+            assert.notOk(v.$el.hasAttribute('v-cloak'))
+        })
+
+    })
+
+    describe('if', function () {
+
+        var v
+
+        it('should create and insert the childVM when value is truthy', function () {
+
+            v = new Vue({
+                template: '<div v-if="ok">{{msg}}</div>',
+                data: {
+                    ok: true,
+                    msg: 'hello'
+                }
+            })
+            assert.strictEqual(v.$el.innerHTML, '<div>hello</div><!--vue-if-->')
+
+        })
+
+        it('should destroy childVM and remove content when value is falsy', function (done) {
+
+            v.ok = false
+            nextTick(function () {
+                assert.strictEqual(v.$el.innerHTML, '<!--vue-if-->')
+                done()
+            })
+
+        })
+
+        it('should work with v-component', function (done) {
+            
+            v = new Vue({
+                template: '<div v-if="ok" v-component="test"></div>',
+                data: {
+                    ok: true,
+                    msg: 'hello'
+                },
+                components: {
+                    test: {
+                        template: '{{msg}}'
+                    }
+                }
+            })
+            assert.strictEqual(v.$el.innerHTML, '<div>hello</div><!--vue-if-->')
+
+            v.ok = false
+            nextTick(function () {
+                assert.strictEqual(v.$el.innerHTML, '<!--vue-if-->')
+                done()
+            })
+
+        })
+
+    })
+
+    describe('view', function () {
+        
+        it('should dynamically switch components', function (done) {
+            
+            var v = new Vue({
+                template: '<div v-view="view" class="view"></div>',
+                data: {
+                    view: 'a'
+                },
+                components: {
+                    a: { template: 'A' },
+                    b: { template: 'B' }
+                }
+            })
+
+            assert.equal(
+                v.$el.innerHTML,
+                '<div class="view">A</div><!--v-view-->'
+            )
+            v.view = 'b'
+
+            nextTick(function () {
+                assert.equal(
+                    v.$el.innerHTML,
+                    '<div class="view">B</div><!--v-view-->'
+                )
+                done()
+            })
+
+        })
+
+    })
+
     // More detailed testing for v-repeat can be found in functional tests.
     // this is mainly for code coverage
     describe('repeat', function () {
@@ -937,101 +1029,6 @@ describe('Directives', function () {
 
     })
 
-    describe('style', function () {
-        
-        it('should apply a normal style', function () {
-            var d = mockDirective('style')
-            d.arg = 'text-align'
-            d.bind()
-            assert.strictEqual(d.prop, 'textAlign')
-            d.update('center')
-            assert.strictEqual(d.el.style.textAlign, 'center')
-        })
-
-        it('should apply prefixed style', function () {
-            var d = mockDirective('style')
-            d.arg = '-webkit-transform'
-            d.bind()
-            assert.strictEqual(d.prop, 'webkitTransform')
-            d.update('scale(2)')
-            assert.strictEqual(d.el.style.webkitTransform, 'scale(2)')
-        })
-
-        it('should auto prefix styles', function () {
-            var d = mockDirective('style')
-            d.arg = '$transform'
-            d.bind()
-            assert.ok(d.prefixed)
-            assert.strictEqual(d.prop, 'transform')
-            var val = 'scale(2)'
-            d.update(val)
-            assert.strictEqual(d.el.style.transform, val)
-            assert.strictEqual(d.el.style.webkitTransform, val)
-            assert.strictEqual(d.el.style.mozTransform, val)
-            assert.strictEqual(d.el.style.msTransform, val)
-        })
-
-        it('should set cssText if no arg', function () {
-            var d = mockDirective('style')
-            d.bind()
-            var val = 'color:#fff'
-            d.update(val)
-            assert.strictEqual(d.el.style.color, 'rgb(255, 255, 255)')
-        })
-
-    })
-
-    describe('cloak', function () {
-        
-        it('should remove itself after the instance is ready', function () {
-            // it doesn't make sense to test with a mock for this one, so...
-            var v = new Vue({
-                template: '<div v-cloak></div>',
-                replace: true,
-                ready: function () {
-                    // this hook is attached before the v-cloak hook
-                    // so it should still have the attribute
-                    assert.ok(this.$el.hasAttribute('v-cloak'))
-                }
-            })
-            assert.notOk(v.$el.hasAttribute('v-cloak'))
-        })
-
-    })
-
-    describe('view', function () {
-        
-        it('should dynamically switch components', function (done) {
-            
-            var v = new Vue({
-                template: '<div v-view="view" class="view"></div>',
-                data: {
-                    view: 'a'
-                },
-                components: {
-                    a: { template: 'A' },
-                    b: { template: 'B' }
-                }
-            })
-
-            assert.equal(
-                v.$el.innerHTML,
-                '<div class="view">A</div><!--v-view-->'
-            )
-            v.view = 'b'
-
-            nextTick(function () {
-                assert.equal(
-                    v.$el.innerHTML,
-                    '<div class="view">B</div><!--v-view-->'
-                )
-                done()
-            })
-
-        })
-
-    })
-
 })
 
 function mockDirective (dirName, tag, type) {