Pārlūkot izejas kodu

Add v-sync directive. (#598)

FuryBean 8 gadi atpakaļ
vecāks
revīzija
112eb485a0
4 mainītis faili ar 210 papildinājumiem un 2 dzēšanām
  1. 3 0
      src/index.js
  2. 54 0
      src/utils/sync.js
  3. 151 0
      test/unit/specs/sync.spec.js
  4. 2 2
      test/unit/util.js

+ 3 - 0
src/index.js

@@ -1,3 +1,4 @@
+import Sync from './utils/sync';
 import Pagination from '../packages/pagination/index.js';
 import Dialog from '../packages/dialog/index.js';
 import Autocomplete from '../packages/autocomplete/index.js';
@@ -58,6 +59,7 @@ const install = function(Vue) {
   /* istanbul ignore if */
   if (install.installed) return;
 
+  Vue.directive('sync', Sync);
   Vue.component(Pagination.name, Pagination);
   Vue.component(Dialog.name, Dialog);
   Vue.component(Autocomplete.name, Autocomplete);
@@ -128,6 +130,7 @@ if (typeof window !== 'undefined' && window.Vue) {
 module.exports = {
   version: '1.0.0-rc.7',
   install,
+  Sync,
   Pagination,
   Dialog,
   Autocomplete,

+ 54 - 0
src/utils/sync.js

@@ -0,0 +1,54 @@
+const SYNC_HOOK_PROP = '$v-sync';
+
+/**
+ * v-sync directive
+ *
+ * Usage:
+ *  v-sync:component-prop="context prop name"
+ *
+ * If your want to sync component's prop "visible" to context prop "myVisible", use like this:
+ *  v-sync:visible="myVisible"
+ */
+export default {
+  bind(el, binding, vnode) {
+    const context = vnode.context;
+    const component = vnode.child;
+    const expression = binding.expression;
+    const prop = binding.arg;
+
+    if (!expression || !prop) {
+      console.warn('v-sync should specify arg & expression, for example: v-sync:visible="myVisible"');
+      return;
+    }
+
+    if (!component || !component.$watch) {
+      console.warn('v-sync is only available on Vue Component');
+      return;
+    }
+
+    const unwatchContext = context.$watch(expression, (val) => {
+      component[prop] = val;
+    });
+
+    const unwatchComponent = component.$watch(prop, (val) => {
+      context[expression] = val;
+    });
+
+    Object.defineProperty(component, SYNC_HOOK_PROP, {
+      value: {
+        unwatchContext,
+        unwatchComponent
+      },
+      enumerable: false
+    });
+  },
+
+  unbind(el, binding, vnode) {
+    const component = vnode.child;
+    if (component && component[SYNC_HOOK_PROP]) {
+      const { unwatchContext, unwatchComponent } = component[SYNC_HOOK_PROP];
+      unwatchContext && unwatchContext();
+      unwatchComponent && unwatchComponent();
+    }
+  }
+};

+ 151 - 0
test/unit/specs/sync.spec.js

@@ -0,0 +1,151 @@
+import { createVue, triggerEvent } from '../util';
+import Sync from 'element-ui/src/utils/sync';
+
+const Test = {
+  template: `<div class="sync-test" v-show="visible">
+    <button @click="visible = false">Hide</button>
+    A test component.
+  </div>`,
+  data() {
+    return {
+      visible: true
+    };
+  }
+};
+
+describe('Sync', () => {
+  it('should not throw when use incorrectly', () => {
+    createVue({
+      template: `
+        <test v-sync>
+        </test>
+      `,
+      components: { Test },
+      directives: { Sync },
+      data() {
+        return {
+          myVisible: true
+        };
+      }
+    });
+
+    createVue({
+      template: `
+        <test v-sync:visible>
+        </test>
+      `,
+      components: { Test },
+      directives: { Sync },
+      data() {
+        return {
+          myVisible: true
+        };
+      }
+    });
+
+    createVue({
+      template: `
+        <test v-sync.visible>
+        </test>
+      `,
+      components: { Test },
+      directives: { Sync },
+      data() {
+        return {
+          myVisible: true
+        };
+      }
+    });
+
+    createVue({
+      template: `
+        <div v-sync:visible="myVisible">
+        </div>
+      `,
+      components: { Test },
+      directives: { Sync },
+      data() {
+        return {
+          myVisible: true
+        };
+      }
+    });
+  });
+
+  it('context variable should change when inner component variable change', (done) => {
+    const vm = createVue({
+      template: `
+        <test v-sync:visible="myVisible">
+        </test>
+      `,
+      components: { Test },
+      directives: { Sync },
+      data() {
+        return {
+          myVisible: true
+        };
+      }
+    }, true);
+
+    triggerEvent(vm.$el.querySelector('.sync-test button'), 'click', {});
+    setTimeout(() => {
+      expect(vm.myVisible).to.be.false;
+      done();
+    }, 10);
+  });
+
+  it('inner component variable should change when context variable change', (done) => {
+    const vm = createVue({
+      template: `
+        <test ref="test" v-sync:visible="myVisible">
+        </test>
+      `,
+      components: { Test },
+      directives: { Sync },
+      data() {
+        return {
+          myVisible: true
+        };
+      }
+    }, true);
+
+    vm.myVisible = false;
+
+    setTimeout(() => {
+      expect(vm.$refs.test.visible).to.be.false;
+      expect(vm.$refs.test.$el.style.display).to.equal('none');
+      done();
+    }, 10);
+  });
+
+  it('should unwatch expression after destroy', () => {
+    const vm = createVue({
+      template: `
+        <test ref="test" v-sync:visible="myVisible" v-if="createTest">
+        </test>
+      `,
+      components: { Test },
+      directives: { Sync },
+      data() {
+        return {
+          myVisible: true,
+          createTest: false
+        };
+      }
+    });
+
+    const beforeBindCount = vm._watchers.length;
+    vm.createTest = true;
+    const delay = 50;
+    setTimeout(() => {
+      const afterBindCount = vm._watchers.length;
+      expect(afterBindCount).to.be.equal(beforeBindCount + 1);
+
+      vm.createTest = false;
+      setTimeout(() => {
+        const afterDestroyCount = vm._watchers.length;
+        expect(afterDestroyCount).to.be.equal(beforeBindCount);
+      }, delay);
+    }, delay);
+  });
+});

+ 2 - 2
test/unit/util.js

@@ -49,7 +49,7 @@ exports.createTest = function(Compo, propsData = {}, mounted = false) {
 
 /**
  * 触发一个事件
- * mouseenter, mouseleave, mouseover, keyup, change 等
+ * mouseenter, mouseleave, mouseover, keyup, change, click
  * @param  {Element} elm
  * @param  {EventName} name
  * @param  {options} opts
@@ -57,7 +57,7 @@ exports.createTest = function(Compo, propsData = {}, mounted = false) {
 exports.triggerEvent = function(elm, name, opts) {
   let eventName;
 
-  if (/^mouse/.test(name)) {
+  if (/^mouse|click/.test(name)) {
     eventName = 'MouseEvents';
   } else if (/^key/.test(name)) {
     eventName = 'KeyboardEvent';