Преглед изворни кода

v-with allows linking keys between child and parent VMs

Evan You пре 12 година
родитељ
комит
1a05deaa37

+ 19 - 10
src/compiler.js

@@ -49,7 +49,7 @@ function Compiler (vm, options) {
     log('\nnew VM instance:', el.tagName, '\n')
 
     // set compiler properties
-    compiler.vm  = vm
+    compiler.vm = el.vue_vm = vm
     compiler.bindings = makeHash()
     compiler.dirs = []
     compiler.deferred = []
@@ -132,7 +132,9 @@ function Compiler (vm, options) {
     compiler.init = false
 
     // post compile / ready hook
-    compiler.execHook('ready')
+    if (!compiler.delayReady) {
+        compiler.execHook('ready')
+    }
 }
 
 var CompilerProto = Compiler.prototype
@@ -304,7 +306,7 @@ CompilerProto.compile = function (node, root) {
 
         // special attributes to check
         var repeatExp,
-            withKey,
+            withExp,
             partialId,
             directive,
             componentId = utils.attr(node, 'component') || tagName.toLowerCase(),
@@ -332,13 +334,19 @@ CompilerProto.compile = function (node, root) {
             }
 
         // v-with has 2nd highest priority
-        } else if (root !== true && ((withKey = utils.attr(node, 'with')) || componentCtor)) {
-
-            directive = Directive.parse('with', withKey || '', compiler, node)
-            if (directive) {
-                directive.Ctor = componentCtor
-                compiler.deferred.push(directive)
-            }
+        } else if (root !== true && ((withExp = utils.attr(node, 'with')) || componentCtor)) {
+
+            withExp = Directive.split(withExp || '')
+            withExp.forEach(function (exp, i) {
+                var directive = Directive.parse('with', exp, compiler, node)
+                if (directive) {
+                    directive.Ctor = componentCtor
+                    // notify the directive that this is the
+                    // last expression in the group
+                    directive.last = i === withExp.length - 1
+                    compiler.deferred.push(directive)
+                }
+            })
 
         } else {
 
@@ -801,6 +809,7 @@ CompilerProto.destroy = function () {
     } else {
         vm.$remove()
     }
+    el.vue_vm = null
 
     this.destroyed = true
     // emit destroy hook

+ 63 - 10
src/directives/with.js

@@ -1,35 +1,88 @@
-var ViewModel
+var ViewModel,
+    nextTick = require('../utils').nextTick
 
 module.exports = {
 
     bind: function () {
-        if (this.isEmpty) {
+        if (this.el.vue_vm) {
+            this.subVM = this.el.vue_vm
+            var compiler = this.subVM.$compiler
+            if (!compiler.bindings[this.arg]) {
+                compiler.createBinding(this.arg)
+            }
+        } else if (this.isEmpty) {
             this.build()
         }
     },
 
-    update: function (value) {
-        if (!this.component) {
+    update: function (value, init) {
+        var vm = this.subVM,
+            key = this.arg || '$data'
+        if (!vm) {
             this.build(value)
-        } else {
-            this.component.$data = value
+        } else if (!this.lock && vm[key] !== value) {
+            vm[key] = value
+        }
+        if (init) {
+            // watch after first set
+            this.watch()
+            // The v-with directive can have multiple expressions,
+            // and we want to make sure when the ready hook is called
+            // on the subVM, all these clauses have been properly set up.
+            // So this is a hack that sniffs whether we have reached
+            // the last expression. We hold off the subVM's ready hook
+            // until we are actually ready.
+            if (this.last) {
+                this.subVM.$compiler.execHook('ready')
+            }
         }
     },
 
     build: function (value) {
         ViewModel = ViewModel || require('../viewmodel')
-        var Ctor = this.Ctor || ViewModel
-        this.component = new Ctor({
+        var Ctor = this.Ctor || ViewModel,
+            data = value
+        if (this.arg) {
+            data = {}
+            data[this.arg] = value
+        }
+        this.subVM = new Ctor({
             el: this.el,
-            data: value,
+            data: data,
             compilerOptions: {
+                // it is important to delay the ready hook
+                // so that when it's called, all `v-with` wathcers
+                // would have been set up.
+                delayReady: !this.last,
                 parentCompiler: this.compiler
             }
         })
     },
 
+    /**
+     *  For inhertied keys, need to watch
+     *  and sync back to the parent
+     */
+    watch: function () {
+        if (!this.arg) return
+        var self    = this,
+            key     = self.key,
+            ownerVM = self.binding.compiler.vm
+        this.subVM.$compiler.observer.on('change:' + this.arg, function (val) {
+            if (!self.lock) {
+                self.lock = true
+                nextTick(function () {
+                    self.lock = false
+                })
+            }
+            ownerVM.$set(key, val)
+        })
+    },
+
     unbind: function () {
-        this.component.$destroy()
+        // all watchers are turned off during destroy
+        // so no need to worry about it
+        this.subVM.$destroy()
     }
 
 }

+ 17 - 4
test/functional/fixtures/component.html

@@ -13,6 +13,13 @@
 
     <!-- custom element alone -->
     <simple id="element"></simple>
+
+    <!-- v-with + binding sync -->
+    <div id="with-sync" v-with="childHi:hi, childName:user.name">
+        {{childHi}} {{childName}}
+    </div>
+
+    <div id="component-with-sync" v-component="sync" v-with="childHi:hi, childName:user.name">
 </div>
 
 <script src="../../../dist/vue.js"></script>
@@ -21,16 +28,22 @@
     Vue.config({debug: true})
 
     Vue.component('avatar', {
-        template: '{{hi}} {{name}}',
-        ready: function () {
-            console.log(JSON.stringify(this))
-        }
+        template: '{{hi}} {{name}}'
     })
 
     Vue.component('simple', {
         template: '{{hi}} {{user.name}}'
     })
 
+    Vue.component('sync', {
+        template: '{{childHi}} {{childName}}',
+        ready: function () {
+            // should sync back to parent
+            this.childHi = 'hello'
+            this.childName = 'Vue'
+        }
+    })
+
     var app = new Vue({
         el: '#test',
         data: {

+ 4 - 2
test/functional/specs/component.js

@@ -1,14 +1,16 @@
-casper.test.begin('Components', 5, function (test) {
+casper.test.begin('Components', 7, function (test) {
     
     casper
     .start('./fixtures/component.html')
     .then(function () {
-        var expected = '123 Jack'
+        var expected = 'hello Vue'
         test.assertSelectorHasText('#component-and-with', expected)
         test.assertSelectorHasText('#element-and-with', expected)
         test.assertSelectorHasText('#component', expected)
         test.assertSelectorHasText('#with', expected)
         test.assertSelectorHasText('#element', expected)
+        test.assertSelectorHasText('#with-sync', expected)
+        test.assertSelectorHasText('#component-with-sync', expected)
     })
     .run(function () {
         test.done()

+ 43 - 0
test/unit/specs/directives.js

@@ -624,6 +624,49 @@ describe('UNIT: Directives', function () {
             assert.strictEqual(t.$el.querySelector('span').textContent, testId)
         })
 
+        it('should accept args and sync parent and child', function (done) {
+            var t = new Vue({
+                template:
+                    '<span>{{test.msg}} {{n}}</span>'
+                    + '<p v-with="childMsg:test.msg, n:n" v-ref="child">{{childMsg}} {{n}}</p>',
+                data: {
+                    n: 1,
+                    test: {
+                        msg: 'haha!'
+                    }
+                }
+            })
+
+            nextTick(function () {
+                assert.strictEqual(t.$el.querySelector('span').textContent, 'haha! 1')
+                assert.strictEqual(t.$el.querySelector('p').textContent, 'haha! 1')
+                testParentToChild()
+            })
+            
+            function testParentToChild () {
+                // test sync from parent to child
+                t.test = { msg: 'hehe!' }
+                nextTick(function () {
+                    assert.strictEqual(t.$el.querySelector('p').textContent, 'hehe! 1')
+                    testChildToParent()
+                })
+            }
+            
+            function testChildToParent () {
+                // test sync back
+                t.$.child.childMsg = 'hoho!'
+                t.$.child.n = 2
+                assert.strictEqual(t.test.msg, 'hoho!')
+                assert.strictEqual(t.n, 2)
+                nextTick(function () {
+                    assert.strictEqual(t.$el.querySelector('span').textContent, 'hoho! 2')
+                    assert.strictEqual(t.$el.querySelector('p').textContent, 'hoho! 2')
+                    done()
+                })
+            }
+
+        })
+
     })
 
     describe('ref', function () {