├── .gitignore ├── .vscode └── settings.json ├── src ├── utils │ ├── isFunction.js │ ├── isNumeric.js │ ├── isObject.js │ ├── upperCaseFirst.js │ └── index.js └── index.js ├── images └── logo.png ├── .travis.yml ├── .editorconfig ├── bdr.config.js ├── LICENSE ├── test ├── enum.test.js ├── localData.test.js ├── numeric.test.js ├── transform.test.js └── on.test.js ├── package.json ├── dist ├── vue-messenger.min.js ├── vue-messenger.es.js ├── vue-messenger.cjs.js └── vue-messenger.js ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *.log 4 | coverage 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "npm-scripts.showStartNotification": false 3 | } -------------------------------------------------------------------------------- /src/utils/isFunction.js: -------------------------------------------------------------------------------- 1 | export default value => typeof value === 'function' 2 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fjc0k/vue-messenger/HEAD/images/logo.png -------------------------------------------------------------------------------- /src/utils/isNumeric.js: -------------------------------------------------------------------------------- 1 | export default value => !isNaN(value - parseFloat(value)) 2 | -------------------------------------------------------------------------------- /src/utils/isObject.js: -------------------------------------------------------------------------------- 1 | export default value => value && typeof value === 'object' 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 9 4 | cache: 5 | directories: 6 | - node_modules 7 | install: 8 | - yarn 9 | script: 10 | - yarn test 11 | after_success: 12 | - yarn codecov 13 | -------------------------------------------------------------------------------- /src/utils/upperCaseFirst.js: -------------------------------------------------------------------------------- 1 | const cache = Object.create(null) 2 | 3 | export default str => { 4 | if (!(str in cache)) { 5 | cache[str] = str.charAt(0).toUpperCase() + str.slice(1) 6 | } 7 | return cache[str] 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export { default as isFunction } from './isFunction' 2 | export { default as isNumeric } from './isNumeric' 3 | export { default as isObject } from './isObject' 4 | export { default as upperCaseFirst } from './upperCaseFirst' 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # The JSON files contain newlines inconsistently 13 | [*.json] 14 | insert_final_newline = ignore 15 | 16 | # Minified JavaScript files shouldn't be changed 17 | [**.min.js] 18 | indent_style = ignore 19 | insert_final_newline = ignore 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | -------------------------------------------------------------------------------- /bdr.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: { 3 | 'vue-messenger': [ 4 | 'src/index.js', 5 | 'VueMessenger' 6 | ] 7 | }, 8 | getUmdMinSize(rawSize, gzippedSize) { 9 | const path = require('path') 10 | const fs = require('fs') 11 | 12 | const tasks = [ 13 | ['README.md', /[^-]+(?=-blue\.svg\?MIN)/, /[^-]+(?=-blue\.svg\?MZIP)/] 14 | // ['README_zh-CN.md', /[^-]+(?=-blue\.svg\?MIN)/, /[^-]+(?=-blue\.svg\?MZIP)/] 15 | ] 16 | 17 | tasks.forEach(task => { 18 | const filePath = path.resolve(__dirname, task[0]) 19 | const content = fs.readFileSync(filePath) 20 | fs.writeFileSync( 21 | filePath, 22 | String(content) 23 | .replace(task[1], encodeURIComponent(rawSize)) 24 | .replace(task[2], encodeURIComponent(gzippedSize)) 25 | ) 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 fjc0k 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/enum.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import Messenger from '../src' 3 | 4 | global.console.error = jest.fn(error => { 5 | throw new Error(error) 6 | }) 7 | 8 | const getComponent = () => { 9 | return { 10 | template: ``, 11 | data: () => ({ 12 | type: 'primary' 13 | }), 14 | components: { 15 | child: { 16 | name: 'child', 17 | mixins: [Messenger], 18 | template: `
`, 19 | props: { 20 | type: { 21 | type: String, 22 | enum: ['default', 'primary', 'danger'] 23 | }, 24 | size: { 25 | type: String, 26 | enum: ['lg', 'sm', 'xs'], 27 | default: 'sm' 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | const wrapper = mount(getComponent()) 36 | const child = wrapper.find({ name: 'child' }) 37 | 38 | test('enum valid', () => { 39 | expect(child.vm.type).toEqual('primary') 40 | expect(child.vm.size).toEqual('sm') 41 | }) 42 | 43 | test('enum unvalid', () => { 44 | expect(() => { 45 | wrapper.vm.type = 'test' 46 | }).toThrow(/custom validator check failed for prop "type"/) 47 | }) 48 | -------------------------------------------------------------------------------- /test/localData.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import Messenger from '../src' 3 | 4 | const getComponent = () => { 5 | return { 6 | template: ``, 7 | data: () => ({ 8 | value: 'foo', 9 | visible: false 10 | }), 11 | components: { 12 | child: { 13 | name: 'child', 14 | mixins: [Messenger], 15 | template: `
`, 16 | props: { 17 | value: String, 18 | visible: { 19 | type: Boolean, 20 | sync: true 21 | } 22 | } 23 | } 24 | } 25 | } 26 | } 27 | 28 | const wrapper = mount(getComponent()) 29 | const child = wrapper.find({ name: 'child' }) 30 | 31 | test('child has correct localData', () => { 32 | expect(child.vm.$data).toEqual( 33 | expect.objectContaining({ 34 | localValue: 'foo', 35 | localVisible: false 36 | }) 37 | ) 38 | }) 39 | 40 | test('change parent data to update child localData', () => { 41 | wrapper.setData({ 42 | value: 'bar', 43 | visible: true 44 | }) 45 | expect(child.vm.$data).toEqual( 46 | expect.objectContaining({ 47 | localValue: 'bar', 48 | localVisible: true 49 | }) 50 | ) 51 | }) 52 | 53 | test('change child localData to update parent data', () => { 54 | child.vm.$data.localValue = 'foo' 55 | child.vm.$data.localVisible = false 56 | expect(wrapper.vm.$data).toEqual( 57 | expect.objectContaining({ 58 | value: 'foo', 59 | visible: false 60 | }) 61 | ) 62 | }) 63 | 64 | test('use sendProp to update parent data', () => { 65 | child.vm.sendValue('bar') 66 | child.vm.sendVisible(true) 67 | expect(wrapper.vm.$data).toEqual( 68 | expect.objectContaining({ 69 | value: 'bar', 70 | visible: true 71 | }) 72 | ) 73 | }) 74 | 75 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-messenger", 3 | "version": "2.3.3", 4 | "description": "A series of useful enhancements to Vue component props.", 5 | "license": "MIT", 6 | "main": "dist/vue-messenger.cjs.js", 7 | "module": "dist/vue-messenger.es.js", 8 | "unpkg": "dist/vue-messenger.min.js", 9 | "jsdelivr": "dist/vue-messenger.min.js", 10 | "homepage": "https://github.com/fjc0k/vue-messenger", 11 | "author": { 12 | "name": "fjc0k", 13 | "email": "fjc0kb@gmail.com", 14 | "url": "https://github.com/fjc0k" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git@github.com:fjc0k/vue-messenger.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/fjc0k/vue-messenger/issues" 22 | }, 23 | "keywords": [ 24 | "vue", 25 | "twoway", 26 | "binding", 27 | "sync", 28 | "model", 29 | "messenger", 30 | "component" 31 | ], 32 | "files": [ 33 | "dist" 34 | ], 35 | "scripts": { 36 | "testOnly": "jest", 37 | "test": "jest --coverage", 38 | "build": "bdr", 39 | "release": "standard-version -a", 40 | "postrelease": "git push --follow-tags origin master && npm publish" 41 | }, 42 | "standard-version": { 43 | "scripts": { 44 | "postbump": "yarn build && git add -A" 45 | } 46 | }, 47 | "eslintConfig": { 48 | "root": true, 49 | "extends": "@fir-ui/fir", 50 | "rules": { 51 | "no-eq-null": 0, 52 | "eqeqeq": 0, 53 | "complexity": 0 54 | } 55 | }, 56 | "eslintIgnore": [ 57 | "dist" 58 | ], 59 | "babel": { 60 | "presets": [ 61 | [ 62 | "@bdr/bdr" 63 | ] 64 | ] 65 | }, 66 | "devDependencies": { 67 | "@babel/core": "^7.0.0-beta.49", 68 | "@fir-ui/eslint-config-fir": "^0.50.4", 69 | "@vue/test-utils": "^1.0.0-beta.18", 70 | "babel-core": "^7.0.0-bridge.0", 71 | "babel-jest": "^23.0.1", 72 | "bdr": "^1.5.0", 73 | "codecov": "^3.0.2", 74 | "eslint": "^4.19.1", 75 | "jest": "^23.1.0", 76 | "sinon": "^6.0.0", 77 | "standard-version": "^4.4.0", 78 | "vue": "^2.5.16", 79 | "vue-template-compiler": "^2.5.16" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/numeric.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import Messenger from '../src' 3 | 4 | global.console.error = jest.fn(error => { 5 | throw new Error(error) 6 | }) 7 | 8 | const getComponent = () => { 9 | return { 10 | template: ``, 11 | data: () => ({ 12 | count: '1', 13 | infinity: 1, 14 | range: 0.5 15 | }), 16 | components: { 17 | child: { 18 | name: 'child', 19 | mixins: [Messenger], 20 | template: `
`, 21 | props: { 22 | count: { 23 | numeric: true, 24 | validator: value => value <= 10 25 | }, 26 | infinity: { 27 | numeric: true, 28 | infinite: true 29 | }, 30 | range: { 31 | numeric: true, 32 | range: [0, 1] 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | 40 | const wrapper = mount(getComponent()) 41 | const child = wrapper.find({ name: 'child' }) 42 | 43 | test('numeric valid', () => { 44 | expect(child.vm.count).toEqual('1') 45 | wrapper.vm.count = 10 46 | expect(child.vm.count).toEqual(10) 47 | wrapper.vm.infinity = -Infinity 48 | expect(child.vm.infinity).toEqual(-Infinity) 49 | wrapper.vm.infinity = Infinity 50 | expect(child.vm.infinity).toEqual(Infinity) 51 | wrapper.vm.range = 1 52 | expect(child.vm.range).toEqual(1) 53 | }) 54 | 55 | test('numeric unvalid', () => { 56 | expect(() => { 57 | wrapper.vm.count = '1@' 58 | }).toThrow(/custom validator check failed for prop "count"/) 59 | wrapper.vm.count = '1' 60 | expect(() => { 61 | wrapper.vm.count = 11 62 | }).toThrow(/custom validator check failed for prop "count"/) 63 | wrapper.vm.count = '1' 64 | expect(() => { 65 | wrapper.vm.count = Infinity 66 | }).toThrow(/custom validator check failed for prop "count"/) 67 | wrapper.vm.count = '1' 68 | expect(() => { 69 | wrapper.vm.range = 1.0001 70 | }).toThrow(/custom validator check failed for prop "range"/) 71 | wrapper.vm.range = 1 72 | expect(() => { 73 | wrapper.vm.range = -0.0000001 74 | }).toThrow(/custom validator check failed for prop "range"/) 75 | }) 76 | -------------------------------------------------------------------------------- /dist/vue-messenger.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-messenger v2.3.3 3 | * (c) 2018-present fjc0k (https://github.com/fjc0k) 4 | * Released under the MIT License. 5 | */ 6 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.VueMessenger=t()}(this,function(){"use strict";var e=function(e){return"function"==typeof e},t=function(e){return e&&"object"==typeof e},n=Object.create(null),i={prop:"value",event:"input"};return{beforeCreate:function(){if(!this.__MessengerBeforeCreate__){this.__MessengerBeforeCreate__=!0;var a=this.$options;a.localDataKeys||(a.localDataKeys=[]),a.methods||(a.methods={}),a.watch||(a.watch={});for(var r=a.model||i,s=a.props||{},o=Object.keys(s),l=function(){var i=o[c],l=s[i];if(Array.isArray(l.enum)){var u=l.validator;l.validator=function(e){return l.enum.indexOf(e)>=0&&(!u||u.apply(this,arguments))},"default"in l||(l.default=l.enum[0])}if(!0===l.numeric){var h=l.validator;l.type=[String,Number],l.validator=function(e){var t=l.infinite,n=l.range;return(t&&(e===1/0||e===-1/0)||function(e){return!isNaN(e-parseFloat(e))}(e))&&(!Array.isArray(n)||e>=n[0]&&e<=n[1])&&(!h||h.apply(this,arguments))}}var f,d=i===r.prop,p=d?r.event:"update:"+i,v=d||!!l.sync,m=!!l.transform,y=l.on&&e(l.on.receive||l.on.change),_=v||m,g=void 0,$=void 0,b=void 0,D=l.on;if((y||_)&&t(D)&&(e(D.receive)&&(g=D.receive),e(D.send)&&($=D.send),e(D.change)&&(b=D.change)),_){var w,A,M=l.transform;e(M)?w=M:t(M)&&(e(M.receive)&&(w=M.receive),e(M.send)&&(A=M.send));var j=((f=i)in n||(n[f]=f.charAt(0).toUpperCase()+f.slice(1)),n[f]),C="local"+j,K="last"+j+"$$",x="lastLocal"+j+"$$",N="send"+j;a.localDataKeys.push(C),a.watch[i]={immediate:!0,handler:function(e,t){e!==t&&e!==this[x]?(!w||null==e||(e=w.call(this,e))!==t&&e!==this[x])&&(g&&!1===g.call(this,e,t)||b&&!1===b.call(this,e,t)||(this[K]=e,this[C]=e)):this[K]=e}},a.watch[C]={immediate:!1,handler:function(e,t){e!==t&&e!==this[K]?(!A||null==e||(e=A.call(this,e))!==t&&e!==this[K])&&($&&!1===$.call(this,e,t)||b&&!1===b.call(this,e,t)||(this[x]=e,v&&this.$emit(p,e,t))):this[x]=e}},v&&(a.methods[N]=function(e){this[C]=e})}else y&&(a.watch[i]={immediate:!0,handler:function(e,t){e!==t&&(g&&g.call(this,e,t),b&&b.call(this,e,t))}})},c=0;c { 5 | return { 6 | template: ``, 7 | data: () => ({ 8 | size: 'lg', 9 | flag: 0 10 | }), 11 | components: { 12 | child: { 13 | name: 'child', 14 | model: { 15 | prop: 'size', 16 | event: 'input' 17 | }, 18 | mixins: [Messenger], 19 | template: `
`, 20 | props: { 21 | size: { 22 | type: String, 23 | transform: value => ({ 24 | lg: 'large', 25 | sm: 'small' 26 | }[value]) 27 | }, 28 | flag: { 29 | type: Number, 30 | sync: true, 31 | transform: { 32 | receive: Boolean, 33 | send: Number 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | 42 | const wrapper = mount(getComponent()) 43 | const child = wrapper.find({ name: 'child' }) 44 | 45 | test('size: lg ==> localSize: large', () => { 46 | expect(child.vm.$data).toEqual( 47 | expect.objectContaining({ 48 | localSize: 'large' 49 | }) 50 | ) 51 | }) 52 | 53 | test('size: sm ==> localSize: small', () => { 54 | wrapper.vm.size = 'sm' 55 | expect(child.vm.$data).toEqual( 56 | expect.objectContaining({ 57 | localSize: 'small' 58 | }) 59 | ) 60 | }) 61 | 62 | test('flag: 0 ==> localFlag: false', () => { 63 | expect(child.vm.$data).toEqual( 64 | expect.objectContaining({ 65 | localFlag: false 66 | }) 67 | ) 68 | }) 69 | 70 | test('flag: 1 ==> localFlag: true', () => { 71 | wrapper.vm.flag = 1 72 | expect(child.vm.$data).toEqual( 73 | expect.objectContaining({ 74 | localFlag: true 75 | }) 76 | ) 77 | }) 78 | 79 | test('localFlag: false ==> flag: 0', () => { 80 | child.vm.localFlag = false 81 | expect(wrapper.vm.$data).toEqual( 82 | expect.objectContaining({ 83 | flag: 0 84 | }) 85 | ) 86 | }) 87 | 88 | test('don\'t apply transformations to nil values', () => { 89 | child.vm.localFlag = null 90 | expect(wrapper.vm.$data).toEqual( 91 | expect.objectContaining({ 92 | flag: null 93 | }) 94 | ) 95 | child.vm.localFlag = undefined 96 | expect(wrapper.vm.$data).toEqual( 97 | expect.objectContaining({ 98 | flag: undefined 99 | }) 100 | ) 101 | }) 102 | -------------------------------------------------------------------------------- /test/on.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import sinon from 'sinon' 3 | import Messenger from '../src' 4 | 5 | const getComponent = (on = {}) => { 6 | return { 7 | template: ``, 8 | data: () => ({ 9 | value: 'foo', 10 | visible: false, 11 | x: 'x' 12 | }), 13 | components: { 14 | child: { 15 | name: 'child', 16 | mixins: [Messenger], 17 | template: `
`, 18 | props: { 19 | value: { 20 | type: String, 21 | on: on.value 22 | }, 23 | visible: { 24 | type: Boolean, 25 | sync: true, 26 | on: on.visible 27 | }, 28 | x: { 29 | on: on.x 30 | } 31 | } 32 | } 33 | } 34 | } 35 | } 36 | 37 | test('immediately call onReceive & onChange', () => { 38 | const on = { 39 | value: { 40 | receive: sinon.stub(), 41 | send: sinon.stub(), 42 | change: sinon.stub() 43 | }, 44 | visible: { 45 | receive: sinon.stub(), 46 | send: sinon.stub(), 47 | change: sinon.stub() 48 | }, 49 | x: { 50 | receive: sinon.stub(), 51 | send: sinon.stub(), 52 | change: sinon.stub() 53 | } 54 | } 55 | mount(getComponent(on)) 56 | expect(on.value.receive.calledOnceWith('foo')).toBe(true) 57 | expect(on.value.send.notCalled).toBe(true) 58 | expect(on.value.change.calledOnceWith('foo')).toBe(true) 59 | expect(on.visible.receive.calledOnceWith(false)).toBe(true) 60 | expect(on.visible.send.notCalled).toBe(true) 61 | expect(on.visible.change.calledOnceWith(false)).toBe(true) 62 | expect(on.x.receive.calledOnceWith('x')).toBe(true) 63 | expect(on.x.send.notCalled).toBe(true) 64 | expect(on.x.change.calledOnceWith('x')).toBe(true) 65 | }) 66 | 67 | test('change data to trigger onReceive & onChange', () => { 68 | const on = { 69 | value: { 70 | receive: sinon.stub(), 71 | send: sinon.stub(), 72 | change: sinon.stub() 73 | }, 74 | visible: { 75 | receive: sinon.stub(), 76 | send: sinon.stub(), 77 | change: sinon.stub() 78 | }, 79 | x: { 80 | receive: sinon.stub(), 81 | send: sinon.stub(), 82 | change: sinon.stub() 83 | } 84 | } 85 | const wrapper = mount(getComponent(on)) 86 | expect(on.value.receive.calledOnceWith('foo')).toBe(true) 87 | expect(on.value.send.notCalled).toBe(true) 88 | expect(on.value.change.calledOnceWith('foo')).toBe(true) 89 | expect(on.visible.receive.calledOnceWith(false)).toBe(true) 90 | expect(on.visible.send.notCalled).toBe(true) 91 | expect(on.visible.change.calledOnceWith(false)).toBe(true) 92 | wrapper.vm.value = 'bar' 93 | expect(on.value.receive.calledTwice).toBe(true) 94 | expect(on.value.send.notCalled).toBe(true) 95 | expect(on.value.change.calledTwice).toBe(true) 96 | wrapper.vm.x = 'y' 97 | expect(on.x.receive.calledTwice).toBe(true) 98 | expect(on.x.send.notCalled).toBe(true) 99 | expect(on.x.change.calledTwice).toBe(true) 100 | }) 101 | 102 | test('change localData to trigger onSend & onChange', () => { 103 | const on = { 104 | value: { 105 | receive: sinon.stub(), 106 | send: sinon.stub(), 107 | change: sinon.stub() 108 | }, 109 | visible: { 110 | receive: sinon.stub(), 111 | send: sinon.stub(), 112 | change: sinon.stub() 113 | } 114 | } 115 | const wrapper = mount(getComponent(on)) 116 | const child = wrapper.find({ name: 'child' }) 117 | expect(on.value.receive.calledOnceWith('foo')).toBe(true) 118 | expect(on.value.send.notCalled).toBe(true) 119 | expect(on.value.change.calledOnceWith('foo')).toBe(true) 120 | expect(on.visible.receive.calledOnceWith(false)).toBe(true) 121 | expect(on.visible.send.notCalled).toBe(true) 122 | expect(on.visible.change.calledOnceWith(false)).toBe(true) 123 | child.vm.localValue = 'bar' 124 | expect(on.value.receive.calledOnce).toBe(true) 125 | expect(on.value.send.calledOnceWith('bar')).toBe(true) 126 | expect(on.value.change.calledTwice).toBe(true) 127 | expect(on.visible.receive.calledOnceWith(false)).toBe(true) 128 | expect(on.visible.send.notCalled).toBe(true) 129 | expect(on.visible.change.calledOnceWith(false)).toBe(true) 130 | }) 131 | 132 | test('prevent default', () => { 133 | const on = { 134 | value: { 135 | receive: value => value !== 'test', 136 | send: value => value !== 'test1', 137 | change: value => value !== 'testAll' 138 | }, 139 | visible: { 140 | receive: sinon.stub(), 141 | send: sinon.stub(), 142 | change: sinon.stub() 143 | } 144 | } 145 | const wrapper = mount(getComponent(on)) 146 | const child = wrapper.find({ name: 'child' }) 147 | wrapper.vm.value = 'bar' 148 | expect(child.vm.localValue).toBe('bar') 149 | wrapper.vm.value = 'test' 150 | expect(child.vm.localValue).toBe('bar') 151 | child.vm.localValue = 'test1' 152 | expect(wrapper.vm.value).toBe('test') 153 | wrapper.vm.value = 'testAll' 154 | expect(child.vm.localValue).toBe('test1') 155 | wrapper.vm.value = '123' 156 | child.vm.localValue = 'testAll' 157 | expect(wrapper.vm.value).toBe('123') 158 | }) 159 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | 6 | ## [2.3.3](https://github.com/fjc0k/vue-messenger/compare/v2.3.2...v2.3.3) (2018-06-24) 7 | 8 | 9 | ### Bug Fixes 10 | 11 | * normalize options.props (close: [#2](https://github.com/fjc0k/vue-messenger/issues/2)) ([3810aa7](https://github.com/fjc0k/vue-messenger/commit/3810aa7)) 12 | 13 | 14 | 15 | 16 | ## [2.3.2](https://github.com/fjc0k/vue-messenger/compare/v2.3.1...v2.3.2) (2018-06-15) 17 | 18 | 19 | 20 | 21 | ## [2.3.1](https://github.com/fjc0k/vue-messenger/compare/v2.3.0...v2.3.1) (2018-06-15) 22 | 23 | 24 | 25 | 26 | # [2.3.0](https://github.com/fjc0k/vue-messenger/compare/v2.2.1...v2.3.0) (2018-06-15) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * **on:** support normal prop ([0332788](https://github.com/fjc0k/vue-messenger/commit/0332788)) 32 | 33 | 34 | ### Features 35 | 36 | * **numeric:** support infinite & range ([72614e4](https://github.com/fjc0k/vue-messenger/commit/72614e4)) 37 | 38 | 39 | 40 | 41 | ## [2.2.1](https://github.com/fjc0k/vue-messenger/compare/v2.2.0...v2.2.1) (2018-06-14) 42 | 43 | 44 | 45 | 46 | # [2.2.0](https://github.com/fjc0k/vue-messenger/compare/v2.1.0...v2.2.0) (2018-06-14) 47 | 48 | 49 | ### Features 50 | 51 | * support numeric-type props ([9bbafb0](https://github.com/fjc0k/vue-messenger/commit/9bbafb0)) 52 | 53 | 54 | 55 | 56 | # [2.1.0](https://github.com/fjc0k/vue-messenger/compare/v2.0.1...v2.1.0) (2018-06-13) 57 | 58 | 59 | ### Features 60 | 61 | * support enum ([13cb398](https://github.com/fjc0k/vue-messenger/commit/13cb398)) 62 | 63 | 64 | 65 | 66 | ## [2.0.1](https://github.com/fjc0k/vue-messenger/compare/v2.0.0...v2.0.1) (2018-06-13) 67 | 68 | 69 | 70 | 71 | # [2.0.0](https://github.com/fjc0k/vue-messenger/compare/v1.3.2...v2.0.0) (2018-06-13) 72 | 73 | 74 | ### Features 75 | 76 | * v2 ([455460a](https://github.com/fjc0k/vue-messenger/commit/455460a)) 77 | 78 | 79 | 80 | 81 | ## [1.3.2](https://github.com/fjc0k/vue-messenger/compare/v1.3.1...v1.3.2) (2018-06-09) 82 | 83 | 84 | ### Bug Fixes 85 | 86 | * use latest transformedLocalProp (close: [#1](https://github.com/fjc0k/vue-messenger/issues/1)) ([cb7e614](https://github.com/fjc0k/vue-messenger/commit/cb7e614)) 87 | 88 | 89 | 90 | 91 | ## [1.3.1](https://github.com/fjc0k/vue-messenger/compare/v1.3.0...v1.3.1) (2018-05-31) 92 | 93 | 94 | ### Bug Fixes 95 | 96 | * allow empty event value ([c9406fa](https://github.com/fjc0k/vue-messenger/commit/c9406fa)) 97 | 98 | 99 | 100 | 101 | # [1.3.0](https://github.com/fjc0k/vue-messenger/compare/v1.2.1...v1.3.0) (2018-05-29) 102 | 103 | 104 | ### Features 105 | 106 | * **props:** support transform option ([1dafba4](https://github.com/fjc0k/vue-messenger/commit/1dafba4)) 107 | 108 | 109 | 110 | 111 | ## [1.2.1](https://github.com/fjc0k/vue-messenger/compare/v1.2.0...v1.2.1) (2018-05-22) 112 | 113 | 114 | 115 | 116 | # [1.2.0](https://github.com/fjc0k/vue-messenger/compare/v1.1.1...v1.2.0) (2018-05-22) 117 | 118 | 119 | ### Features 120 | 121 | * shallow copy arrays ([02fb43d](https://github.com/fjc0k/vue-messenger/commit/02fb43d)) 122 | 123 | 124 | 125 | 126 | ## [1.1.1](https://github.com/fjc0k/vue-messenger/compare/v1.1.0...v1.1.1) (2018-05-21) 127 | 128 | 129 | ### Bug Fixes 130 | 131 | * reset transformedProp ([a928f1e](https://github.com/fjc0k/vue-messenger/commit/a928f1e)) 132 | 133 | 134 | 135 | 136 | # [1.1.0](https://github.com/fjc0k/vue-messenger/compare/v1.0.0...v1.1.0) (2018-05-21) 137 | 138 | 139 | ### Features 140 | 141 | * support watch ([4c556fd](https://github.com/fjc0k/vue-messenger/commit/4c556fd)) 142 | 143 | 144 | 145 | 146 | # [1.0.0](https://github.com/fjc0k/vue-messenger/compare/v0.2.3...v1.0.0) (2018-05-05) 147 | 148 | 149 | 150 | 151 | ## [0.2.3](https://github.com/fjc0k/vue-messenger/compare/v0.2.2...v0.2.3) (2018-05-05) 152 | 153 | 154 | 155 | 156 | ## [0.2.2](https://github.com/fjc0k/vue-messenger/compare/v0.2.1...v0.2.2) (2018-05-05) 157 | 158 | 159 | 160 | 161 | ## [0.2.1](https://github.com/fjc0k/vue-messenger/compare/v0.2.0...v0.2.1) (2018-05-05) 162 | 163 | 164 | 165 | 166 | # [0.2.0](https://github.com/fjc0k/vue-messenger/compare/v0.1.4...v0.2.0) (2018-05-05) 167 | 168 | 169 | ### Bug Fixes 170 | 171 | * CI ([80487d4](https://github.com/fjc0k/vue-messenger/commit/80487d4)) 172 | 173 | 174 | ### Features 175 | 176 | * add tests ([7e7db86](https://github.com/fjc0k/vue-messenger/commit/7e7db86)) 177 | 178 | 179 | 180 | 181 | ## [0.1.4](https://github.com/fjc0k/vue-messenger/compare/v0.1.3...v0.1.4) (2018-05-05) 182 | 183 | 184 | 185 | 186 | ## [0.1.3](https://github.com/fjc0k/vue-messenger/compare/v0.1.2...v0.1.3) (2018-05-04) 187 | 188 | 189 | 190 | 191 | ## [0.1.2](https://github.com/fjc0k/vue-messenger/compare/v0.1.1...v0.1.2) (2018-05-04) 192 | 193 | 194 | 195 | 196 | ## [0.1.1](https://github.com/fjc0k/vue-messenger/compare/v0.1.0...v0.1.1) (2018-05-04) 197 | 198 | 199 | 200 | 201 | # 0.1.0 (2018-05-04) 202 | 203 | 204 | ### Features 205 | 206 | * first commit ([de150d8](https://github.com/fjc0k/vue-messenger/commit/de150d8)) 207 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { upperCaseFirst, isFunction, isObject, isNumeric } from './utils' 2 | 3 | const defaultModel = { 4 | prop: 'value', 5 | event: 'input' 6 | } 7 | 8 | export default { 9 | beforeCreate() { 10 | if (this.__MessengerBeforeCreate__) return 11 | 12 | this.__MessengerBeforeCreate__ = true 13 | 14 | const options = this.$options 15 | 16 | if (!options.localDataKeys) options.localDataKeys = [] 17 | if (!options.methods) options.methods = {} 18 | if (!options.watch) options.watch = {} 19 | 20 | const model = options.model || defaultModel 21 | 22 | const props = options.props || {} 23 | 24 | for (const prop of Object.keys(props)) { 25 | const descriptor = props[prop] 26 | 27 | // enum 28 | if (Array.isArray(descriptor.enum)) { 29 | const nextValidator = descriptor.validator 30 | descriptor.validator = function (value) { 31 | return descriptor.enum.indexOf(value) >= 0 && ( 32 | nextValidator ? nextValidator.apply(this, arguments) : true 33 | ) 34 | } 35 | if (!('default' in descriptor)) { 36 | descriptor.default = descriptor.enum[0] 37 | } 38 | } 39 | 40 | // numeric 41 | if (descriptor.numeric === true) { 42 | const nextValidator = descriptor.validator 43 | descriptor.type = [String, Number] 44 | descriptor.validator = function (value) { 45 | const { infinite, range } = descriptor 46 | return ( 47 | ( 48 | infinite && ( 49 | value === Infinity || 50 | value === -Infinity 51 | ) 52 | ) || isNumeric(value) 53 | ) && ( 54 | Array.isArray(range) ? ( 55 | value >= range[0] && 56 | value <= range[1] 57 | ) : true 58 | ) && ( 59 | nextValidator ? nextValidator.apply(this, arguments) : true 60 | ) 61 | } 62 | } 63 | 64 | const isModelProp = prop === model.prop 65 | 66 | const event = isModelProp ? model.event : `update:${prop}` 67 | const shouldEmit = isModelProp || !!descriptor.sync 68 | const shouldTransform = !!descriptor.transform 69 | const shouldListen = ( 70 | descriptor.on && isFunction( 71 | descriptor.on.receive || 72 | descriptor.on.change 73 | ) 74 | ) 75 | const shouldProcess = shouldEmit || shouldTransform 76 | 77 | let onReceive 78 | let onSend 79 | let onChange 80 | const on = descriptor.on 81 | if ((shouldListen || shouldProcess) && isObject(on)) { 82 | if (isFunction(on.receive)) { 83 | onReceive = on.receive 84 | } 85 | if (isFunction(on.send)) { 86 | onSend = on.send 87 | } 88 | if (isFunction(on.change)) { 89 | onChange = on.change 90 | } 91 | } 92 | 93 | if (shouldProcess) { 94 | let receiveTransform 95 | let sendTransform 96 | const transform = descriptor.transform 97 | if (isFunction(transform)) { 98 | receiveTransform = transform 99 | } else if (isObject(transform)) { 100 | if (isFunction(transform.receive)) { 101 | receiveTransform = transform.receive 102 | } 103 | if (isFunction(transform.send)) { 104 | sendTransform = transform.send 105 | } 106 | } 107 | 108 | const Prop = upperCaseFirst(prop) 109 | const localProp = `local${Prop}` 110 | const lastProp = `last${Prop}$$` 111 | const lastLocalProp = `lastLocal${Prop}$$` 112 | const sendProp = `send${Prop}` 113 | 114 | options.localDataKeys.push(localProp) 115 | 116 | options.watch[prop] = { 117 | immediate: true, 118 | handler(newValue, oldValue) { 119 | if (newValue === oldValue || newValue === this[lastLocalProp]) { 120 | this[lastProp] = newValue 121 | return 122 | } 123 | 124 | if (receiveTransform && newValue != null) { 125 | newValue = receiveTransform.call(this, newValue) 126 | 127 | if (newValue === oldValue || newValue === this[lastLocalProp]) return 128 | } 129 | 130 | if (onReceive) { 131 | if (onReceive.call(this, newValue, oldValue) === false) { 132 | return 133 | } 134 | } 135 | 136 | if (onChange) { 137 | if (onChange.call(this, newValue, oldValue) === false) { 138 | return 139 | } 140 | } 141 | 142 | this[lastProp] = newValue 143 | 144 | this[localProp] = newValue 145 | } 146 | } 147 | 148 | options.watch[localProp] = { 149 | immediate: false, 150 | handler(newValue, oldValue) { 151 | if (newValue === oldValue || newValue === this[lastProp]) { 152 | this[lastLocalProp] = newValue 153 | return 154 | } 155 | 156 | if (sendTransform && newValue != null) { 157 | newValue = sendTransform.call(this, newValue) 158 | 159 | if (newValue === oldValue || newValue === this[lastProp]) return 160 | } 161 | 162 | if (onSend) { 163 | if (onSend.call(this, newValue, oldValue) === false) { 164 | return 165 | } 166 | } 167 | 168 | if (onChange) { 169 | if (onChange.call(this, newValue, oldValue) === false) { 170 | return 171 | } 172 | } 173 | 174 | this[lastLocalProp] = newValue 175 | 176 | if (shouldEmit) { 177 | this.$emit(event, newValue, oldValue) 178 | } 179 | } 180 | } 181 | 182 | if (shouldEmit) { 183 | options.methods[sendProp] = function (newValue) { 184 | this[localProp] = newValue 185 | } 186 | } 187 | } else if (shouldListen) { 188 | options.watch[prop] = { 189 | immediate: true, 190 | handler(newValue, oldValue) { 191 | if (newValue === oldValue) return 192 | if (onReceive) { 193 | onReceive.call(this, newValue, oldValue) 194 | } 195 | if (onChange) { 196 | onChange.call(this, newValue, oldValue) 197 | } 198 | } 199 | } 200 | } 201 | } 202 | }, 203 | 204 | data() { 205 | if (this.__MessengerData__) return 206 | 207 | this.__MessengerData__ = true 208 | 209 | return this.$options.localDataKeys.reduce((data, key) => { 210 | data[key] = null 211 | return data 212 | }, {}) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /dist/vue-messenger.es.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-messenger v2.3.3 3 | * (c) 2018-present fjc0k (https://github.com/fjc0k) 4 | * Released under the MIT License. 5 | */ 6 | var isFunction = (function (value) { 7 | return typeof value === 'function'; 8 | }); 9 | 10 | var isNumeric = (function (value) { 11 | return !isNaN(value - parseFloat(value)); 12 | }); 13 | 14 | var isObject = (function (value) { 15 | return value && typeof value === 'object'; 16 | }); 17 | 18 | var cache = Object.create(null); 19 | var upperCaseFirst = (function (str) { 20 | if (!(str in cache)) { 21 | cache[str] = str.charAt(0).toUpperCase() + str.slice(1); 22 | } 23 | 24 | return cache[str]; 25 | }); 26 | 27 | var defaultModel = { 28 | prop: 'value', 29 | event: 'input' 30 | }; 31 | var index = { 32 | beforeCreate: function beforeCreate() { 33 | if (this.__MessengerBeforeCreate__) return; 34 | this.__MessengerBeforeCreate__ = true; 35 | var options = this.$options; 36 | if (!options.localDataKeys) options.localDataKeys = []; 37 | if (!options.methods) options.methods = {}; 38 | if (!options.watch) options.watch = {}; 39 | var model = options.model || defaultModel; 40 | var props = options.props || {}; 41 | 42 | var _arr = Object.keys(props); 43 | 44 | var _loop = function _loop() { 45 | var prop = _arr[_i]; 46 | var descriptor = props[prop]; // enum 47 | 48 | if (Array.isArray(descriptor.enum)) { 49 | var nextValidator = descriptor.validator; 50 | 51 | descriptor.validator = function (value) { 52 | return descriptor.enum.indexOf(value) >= 0 && (nextValidator ? nextValidator.apply(this, arguments) : true); 53 | }; 54 | 55 | if (!('default' in descriptor)) { 56 | descriptor.default = descriptor.enum[0]; 57 | } 58 | } // numeric 59 | 60 | 61 | if (descriptor.numeric === true) { 62 | var _nextValidator = descriptor.validator; 63 | descriptor.type = [String, Number]; 64 | 65 | descriptor.validator = function (value) { 66 | var infinite = descriptor.infinite, 67 | range = descriptor.range; 68 | return (infinite && (value === Infinity || value === -Infinity) || isNumeric(value)) && (Array.isArray(range) ? value >= range[0] && value <= range[1] : true) && (_nextValidator ? _nextValidator.apply(this, arguments) : true); 69 | }; 70 | } 71 | 72 | var isModelProp = prop === model.prop; 73 | var event = isModelProp ? model.event : "update:" + prop; 74 | var shouldEmit = isModelProp || !!descriptor.sync; 75 | var shouldTransform = !!descriptor.transform; 76 | var shouldListen = descriptor.on && isFunction(descriptor.on.receive || descriptor.on.change); 77 | var shouldProcess = shouldEmit || shouldTransform; 78 | var onReceive = void 0; 79 | var onSend = void 0; 80 | var onChange = void 0; 81 | var on = descriptor.on; 82 | 83 | if ((shouldListen || shouldProcess) && isObject(on)) { 84 | if (isFunction(on.receive)) { 85 | onReceive = on.receive; 86 | } 87 | 88 | if (isFunction(on.send)) { 89 | onSend = on.send; 90 | } 91 | 92 | if (isFunction(on.change)) { 93 | onChange = on.change; 94 | } 95 | } 96 | 97 | if (shouldProcess) { 98 | var receiveTransform; 99 | var sendTransform; 100 | var transform = descriptor.transform; 101 | 102 | if (isFunction(transform)) { 103 | receiveTransform = transform; 104 | } else if (isObject(transform)) { 105 | if (isFunction(transform.receive)) { 106 | receiveTransform = transform.receive; 107 | } 108 | 109 | if (isFunction(transform.send)) { 110 | sendTransform = transform.send; 111 | } 112 | } 113 | 114 | var Prop = upperCaseFirst(prop); 115 | var localProp = "local" + Prop; 116 | var lastProp = "last" + Prop + "$$"; 117 | var lastLocalProp = "lastLocal" + Prop + "$$"; 118 | var sendProp = "send" + Prop; 119 | options.localDataKeys.push(localProp); 120 | options.watch[prop] = { 121 | immediate: true, 122 | handler: function handler(newValue, oldValue) { 123 | if (newValue === oldValue || newValue === this[lastLocalProp]) { 124 | this[lastProp] = newValue; 125 | return; 126 | } 127 | 128 | if (receiveTransform && newValue != null) { 129 | newValue = receiveTransform.call(this, newValue); 130 | if (newValue === oldValue || newValue === this[lastLocalProp]) return; 131 | } 132 | 133 | if (onReceive) { 134 | if (onReceive.call(this, newValue, oldValue) === false) { 135 | return; 136 | } 137 | } 138 | 139 | if (onChange) { 140 | if (onChange.call(this, newValue, oldValue) === false) { 141 | return; 142 | } 143 | } 144 | 145 | this[lastProp] = newValue; 146 | this[localProp] = newValue; 147 | } 148 | }; 149 | options.watch[localProp] = { 150 | immediate: false, 151 | handler: function handler(newValue, oldValue) { 152 | if (newValue === oldValue || newValue === this[lastProp]) { 153 | this[lastLocalProp] = newValue; 154 | return; 155 | } 156 | 157 | if (sendTransform && newValue != null) { 158 | newValue = sendTransform.call(this, newValue); 159 | if (newValue === oldValue || newValue === this[lastProp]) return; 160 | } 161 | 162 | if (onSend) { 163 | if (onSend.call(this, newValue, oldValue) === false) { 164 | return; 165 | } 166 | } 167 | 168 | if (onChange) { 169 | if (onChange.call(this, newValue, oldValue) === false) { 170 | return; 171 | } 172 | } 173 | 174 | this[lastLocalProp] = newValue; 175 | 176 | if (shouldEmit) { 177 | this.$emit(event, newValue, oldValue); 178 | } 179 | } 180 | }; 181 | 182 | if (shouldEmit) { 183 | options.methods[sendProp] = function (newValue) { 184 | this[localProp] = newValue; 185 | }; 186 | } 187 | } else if (shouldListen) { 188 | options.watch[prop] = { 189 | immediate: true, 190 | handler: function handler(newValue, oldValue) { 191 | if (newValue === oldValue) return; 192 | 193 | if (onReceive) { 194 | onReceive.call(this, newValue, oldValue); 195 | } 196 | 197 | if (onChange) { 198 | onChange.call(this, newValue, oldValue); 199 | } 200 | } 201 | }; 202 | } 203 | }; 204 | 205 | for (var _i = 0; _i < _arr.length; _i++) { 206 | _loop(); 207 | } 208 | }, 209 | data: function data() { 210 | if (this.__MessengerData__) return; 211 | this.__MessengerData__ = true; 212 | return this.$options.localDataKeys.reduce(function (data, key) { 213 | data[key] = null; 214 | return data; 215 | }, {}); 216 | } 217 | }; 218 | 219 | export default index; 220 | -------------------------------------------------------------------------------- /dist/vue-messenger.cjs.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-messenger v2.3.3 3 | * (c) 2018-present fjc0k (https://github.com/fjc0k) 4 | * Released under the MIT License. 5 | */ 6 | 'use strict'; 7 | 8 | var isFunction = (function (value) { 9 | return typeof value === 'function'; 10 | }); 11 | 12 | var isNumeric = (function (value) { 13 | return !isNaN(value - parseFloat(value)); 14 | }); 15 | 16 | var isObject = (function (value) { 17 | return value && typeof value === 'object'; 18 | }); 19 | 20 | var cache = Object.create(null); 21 | var upperCaseFirst = (function (str) { 22 | if (!(str in cache)) { 23 | cache[str] = str.charAt(0).toUpperCase() + str.slice(1); 24 | } 25 | 26 | return cache[str]; 27 | }); 28 | 29 | var defaultModel = { 30 | prop: 'value', 31 | event: 'input' 32 | }; 33 | var index = { 34 | beforeCreate: function beforeCreate() { 35 | if (this.__MessengerBeforeCreate__) return; 36 | this.__MessengerBeforeCreate__ = true; 37 | var options = this.$options; 38 | if (!options.localDataKeys) options.localDataKeys = []; 39 | if (!options.methods) options.methods = {}; 40 | if (!options.watch) options.watch = {}; 41 | var model = options.model || defaultModel; 42 | var props = options.props || {}; 43 | 44 | var _arr = Object.keys(props); 45 | 46 | var _loop = function _loop() { 47 | var prop = _arr[_i]; 48 | var descriptor = props[prop]; // enum 49 | 50 | if (Array.isArray(descriptor.enum)) { 51 | var nextValidator = descriptor.validator; 52 | 53 | descriptor.validator = function (value) { 54 | return descriptor.enum.indexOf(value) >= 0 && (nextValidator ? nextValidator.apply(this, arguments) : true); 55 | }; 56 | 57 | if (!('default' in descriptor)) { 58 | descriptor.default = descriptor.enum[0]; 59 | } 60 | } // numeric 61 | 62 | 63 | if (descriptor.numeric === true) { 64 | var _nextValidator = descriptor.validator; 65 | descriptor.type = [String, Number]; 66 | 67 | descriptor.validator = function (value) { 68 | var infinite = descriptor.infinite, 69 | range = descriptor.range; 70 | return (infinite && (value === Infinity || value === -Infinity) || isNumeric(value)) && (Array.isArray(range) ? value >= range[0] && value <= range[1] : true) && (_nextValidator ? _nextValidator.apply(this, arguments) : true); 71 | }; 72 | } 73 | 74 | var isModelProp = prop === model.prop; 75 | var event = isModelProp ? model.event : "update:" + prop; 76 | var shouldEmit = isModelProp || !!descriptor.sync; 77 | var shouldTransform = !!descriptor.transform; 78 | var shouldListen = descriptor.on && isFunction(descriptor.on.receive || descriptor.on.change); 79 | var shouldProcess = shouldEmit || shouldTransform; 80 | var onReceive = void 0; 81 | var onSend = void 0; 82 | var onChange = void 0; 83 | var on = descriptor.on; 84 | 85 | if ((shouldListen || shouldProcess) && isObject(on)) { 86 | if (isFunction(on.receive)) { 87 | onReceive = on.receive; 88 | } 89 | 90 | if (isFunction(on.send)) { 91 | onSend = on.send; 92 | } 93 | 94 | if (isFunction(on.change)) { 95 | onChange = on.change; 96 | } 97 | } 98 | 99 | if (shouldProcess) { 100 | var receiveTransform; 101 | var sendTransform; 102 | var transform = descriptor.transform; 103 | 104 | if (isFunction(transform)) { 105 | receiveTransform = transform; 106 | } else if (isObject(transform)) { 107 | if (isFunction(transform.receive)) { 108 | receiveTransform = transform.receive; 109 | } 110 | 111 | if (isFunction(transform.send)) { 112 | sendTransform = transform.send; 113 | } 114 | } 115 | 116 | var Prop = upperCaseFirst(prop); 117 | var localProp = "local" + Prop; 118 | var lastProp = "last" + Prop + "$$"; 119 | var lastLocalProp = "lastLocal" + Prop + "$$"; 120 | var sendProp = "send" + Prop; 121 | options.localDataKeys.push(localProp); 122 | options.watch[prop] = { 123 | immediate: true, 124 | handler: function handler(newValue, oldValue) { 125 | if (newValue === oldValue || newValue === this[lastLocalProp]) { 126 | this[lastProp] = newValue; 127 | return; 128 | } 129 | 130 | if (receiveTransform && newValue != null) { 131 | newValue = receiveTransform.call(this, newValue); 132 | if (newValue === oldValue || newValue === this[lastLocalProp]) return; 133 | } 134 | 135 | if (onReceive) { 136 | if (onReceive.call(this, newValue, oldValue) === false) { 137 | return; 138 | } 139 | } 140 | 141 | if (onChange) { 142 | if (onChange.call(this, newValue, oldValue) === false) { 143 | return; 144 | } 145 | } 146 | 147 | this[lastProp] = newValue; 148 | this[localProp] = newValue; 149 | } 150 | }; 151 | options.watch[localProp] = { 152 | immediate: false, 153 | handler: function handler(newValue, oldValue) { 154 | if (newValue === oldValue || newValue === this[lastProp]) { 155 | this[lastLocalProp] = newValue; 156 | return; 157 | } 158 | 159 | if (sendTransform && newValue != null) { 160 | newValue = sendTransform.call(this, newValue); 161 | if (newValue === oldValue || newValue === this[lastProp]) return; 162 | } 163 | 164 | if (onSend) { 165 | if (onSend.call(this, newValue, oldValue) === false) { 166 | return; 167 | } 168 | } 169 | 170 | if (onChange) { 171 | if (onChange.call(this, newValue, oldValue) === false) { 172 | return; 173 | } 174 | } 175 | 176 | this[lastLocalProp] = newValue; 177 | 178 | if (shouldEmit) { 179 | this.$emit(event, newValue, oldValue); 180 | } 181 | } 182 | }; 183 | 184 | if (shouldEmit) { 185 | options.methods[sendProp] = function (newValue) { 186 | this[localProp] = newValue; 187 | }; 188 | } 189 | } else if (shouldListen) { 190 | options.watch[prop] = { 191 | immediate: true, 192 | handler: function handler(newValue, oldValue) { 193 | if (newValue === oldValue) return; 194 | 195 | if (onReceive) { 196 | onReceive.call(this, newValue, oldValue); 197 | } 198 | 199 | if (onChange) { 200 | onChange.call(this, newValue, oldValue); 201 | } 202 | } 203 | }; 204 | } 205 | }; 206 | 207 | for (var _i = 0; _i < _arr.length; _i++) { 208 | _loop(); 209 | } 210 | }, 211 | data: function data() { 212 | if (this.__MessengerData__) return; 213 | this.__MessengerData__ = true; 214 | return this.$options.localDataKeys.reduce(function (data, key) { 215 | data[key] = null; 216 | return data; 217 | }, {}); 218 | } 219 | }; 220 | 221 | module.exports = index; 222 | -------------------------------------------------------------------------------- /dist/vue-messenger.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * vue-messenger v2.3.3 3 | * (c) 2018-present fjc0k (https://github.com/fjc0k) 4 | * Released under the MIT License. 5 | */ 6 | (function (global, factory) { 7 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 8 | typeof define === 'function' && define.amd ? define(factory) : 9 | (global.VueMessenger = factory()); 10 | }(this, (function () { 'use strict'; 11 | 12 | var isFunction = (function (value) { 13 | return typeof value === 'function'; 14 | }); 15 | 16 | var isNumeric = (function (value) { 17 | return !isNaN(value - parseFloat(value)); 18 | }); 19 | 20 | var isObject = (function (value) { 21 | return value && typeof value === 'object'; 22 | }); 23 | 24 | var cache = Object.create(null); 25 | var upperCaseFirst = (function (str) { 26 | if (!(str in cache)) { 27 | cache[str] = str.charAt(0).toUpperCase() + str.slice(1); 28 | } 29 | 30 | return cache[str]; 31 | }); 32 | 33 | var defaultModel = { 34 | prop: 'value', 35 | event: 'input' 36 | }; 37 | var index = { 38 | beforeCreate: function beforeCreate() { 39 | if (this.__MessengerBeforeCreate__) return; 40 | this.__MessengerBeforeCreate__ = true; 41 | var options = this.$options; 42 | if (!options.localDataKeys) options.localDataKeys = []; 43 | if (!options.methods) options.methods = {}; 44 | if (!options.watch) options.watch = {}; 45 | var model = options.model || defaultModel; 46 | var props = options.props || {}; 47 | 48 | var _arr = Object.keys(props); 49 | 50 | var _loop = function _loop() { 51 | var prop = _arr[_i]; 52 | var descriptor = props[prop]; // enum 53 | 54 | if (Array.isArray(descriptor.enum)) { 55 | var nextValidator = descriptor.validator; 56 | 57 | descriptor.validator = function (value) { 58 | return descriptor.enum.indexOf(value) >= 0 && (nextValidator ? nextValidator.apply(this, arguments) : true); 59 | }; 60 | 61 | if (!('default' in descriptor)) { 62 | descriptor.default = descriptor.enum[0]; 63 | } 64 | } // numeric 65 | 66 | 67 | if (descriptor.numeric === true) { 68 | var _nextValidator = descriptor.validator; 69 | descriptor.type = [String, Number]; 70 | 71 | descriptor.validator = function (value) { 72 | var infinite = descriptor.infinite, 73 | range = descriptor.range; 74 | return (infinite && (value === Infinity || value === -Infinity) || isNumeric(value)) && (Array.isArray(range) ? value >= range[0] && value <= range[1] : true) && (_nextValidator ? _nextValidator.apply(this, arguments) : true); 75 | }; 76 | } 77 | 78 | var isModelProp = prop === model.prop; 79 | var event = isModelProp ? model.event : "update:" + prop; 80 | var shouldEmit = isModelProp || !!descriptor.sync; 81 | var shouldTransform = !!descriptor.transform; 82 | var shouldListen = descriptor.on && isFunction(descriptor.on.receive || descriptor.on.change); 83 | var shouldProcess = shouldEmit || shouldTransform; 84 | var onReceive = void 0; 85 | var onSend = void 0; 86 | var onChange = void 0; 87 | var on = descriptor.on; 88 | 89 | if ((shouldListen || shouldProcess) && isObject(on)) { 90 | if (isFunction(on.receive)) { 91 | onReceive = on.receive; 92 | } 93 | 94 | if (isFunction(on.send)) { 95 | onSend = on.send; 96 | } 97 | 98 | if (isFunction(on.change)) { 99 | onChange = on.change; 100 | } 101 | } 102 | 103 | if (shouldProcess) { 104 | var receiveTransform; 105 | var sendTransform; 106 | var transform = descriptor.transform; 107 | 108 | if (isFunction(transform)) { 109 | receiveTransform = transform; 110 | } else if (isObject(transform)) { 111 | if (isFunction(transform.receive)) { 112 | receiveTransform = transform.receive; 113 | } 114 | 115 | if (isFunction(transform.send)) { 116 | sendTransform = transform.send; 117 | } 118 | } 119 | 120 | var Prop = upperCaseFirst(prop); 121 | var localProp = "local" + Prop; 122 | var lastProp = "last" + Prop + "$$"; 123 | var lastLocalProp = "lastLocal" + Prop + "$$"; 124 | var sendProp = "send" + Prop; 125 | options.localDataKeys.push(localProp); 126 | options.watch[prop] = { 127 | immediate: true, 128 | handler: function handler(newValue, oldValue) { 129 | if (newValue === oldValue || newValue === this[lastLocalProp]) { 130 | this[lastProp] = newValue; 131 | return; 132 | } 133 | 134 | if (receiveTransform && newValue != null) { 135 | newValue = receiveTransform.call(this, newValue); 136 | if (newValue === oldValue || newValue === this[lastLocalProp]) return; 137 | } 138 | 139 | if (onReceive) { 140 | if (onReceive.call(this, newValue, oldValue) === false) { 141 | return; 142 | } 143 | } 144 | 145 | if (onChange) { 146 | if (onChange.call(this, newValue, oldValue) === false) { 147 | return; 148 | } 149 | } 150 | 151 | this[lastProp] = newValue; 152 | this[localProp] = newValue; 153 | } 154 | }; 155 | options.watch[localProp] = { 156 | immediate: false, 157 | handler: function handler(newValue, oldValue) { 158 | if (newValue === oldValue || newValue === this[lastProp]) { 159 | this[lastLocalProp] = newValue; 160 | return; 161 | } 162 | 163 | if (sendTransform && newValue != null) { 164 | newValue = sendTransform.call(this, newValue); 165 | if (newValue === oldValue || newValue === this[lastProp]) return; 166 | } 167 | 168 | if (onSend) { 169 | if (onSend.call(this, newValue, oldValue) === false) { 170 | return; 171 | } 172 | } 173 | 174 | if (onChange) { 175 | if (onChange.call(this, newValue, oldValue) === false) { 176 | return; 177 | } 178 | } 179 | 180 | this[lastLocalProp] = newValue; 181 | 182 | if (shouldEmit) { 183 | this.$emit(event, newValue, oldValue); 184 | } 185 | } 186 | }; 187 | 188 | if (shouldEmit) { 189 | options.methods[sendProp] = function (newValue) { 190 | this[localProp] = newValue; 191 | }; 192 | } 193 | } else if (shouldListen) { 194 | options.watch[prop] = { 195 | immediate: true, 196 | handler: function handler(newValue, oldValue) { 197 | if (newValue === oldValue) return; 198 | 199 | if (onReceive) { 200 | onReceive.call(this, newValue, oldValue); 201 | } 202 | 203 | if (onChange) { 204 | onChange.call(this, newValue, oldValue); 205 | } 206 | } 207 | }; 208 | } 209 | }; 210 | 211 | for (var _i = 0; _i < _arr.length; _i++) { 212 | _loop(); 213 | } 214 | }, 215 | data: function data() { 216 | if (this.__MessengerData__) return; 217 | this.__MessengerData__ = true; 218 | return this.$options.localDataKeys.reduce(function (data, key) { 219 | data[key] = null; 220 | return data; 221 | }, {}); 222 | } 223 | }; 224 | 225 | return index; 226 | 227 | }))); 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Vue Messenger logo

2 | 3 |

4 | Build Status 5 | Coverage Status 6 | Minified Size 7 | Minzipped Size 8 | Version 9 | License 10 |

11 | 12 | # Vue Messenger 13 | 14 | A series of useful enhancements to Vue components props: 15 | 16 | - [Transform props](#transform-props) 17 | - [Enum-type props](#enum-type-props) 18 | - [Numeric-type props](#numeric-type-props) 19 | - [Listen for receiving props](#listen-for-receiving-props) 20 | - [Two-way data binding props](#two-way-data-binding-props) 21 | 22 | ## Install 23 | 24 | ### Package 25 | 26 | ```bash 27 | # yarn 28 | yarn add vue-messenger 29 | 30 | # or, npm 31 | npm i vue-messenger --save 32 | ``` 33 | 34 | ### CDN 35 | 36 | - [jsDelivr](//www.jsdelivr.com/package/npm/vue-messenger) 37 | - [UNPKG](//unpkg.com/vue-messenger/dist/) 38 | 39 | Available as global `VueMessenger`. 40 | 41 | ## Usage 42 | 43 | ### Install mixin 44 | 45 | #### Globally 46 | 47 | ```js 48 | // main.js 49 | import Vue from 'vue' 50 | import Messenger from 'vue-messenger' 51 | 52 | Vue.mixin(Messenger) 53 | ``` 54 | 55 | #### Locally 56 | 57 | ```js 58 | // Component.vue 59 | import Messenger from 'vue-messenger' 60 | 61 | export default { 62 | mixins: [Messenger], 63 | // ... 64 | } 65 | ``` 66 | 67 | ### Transform props 68 | 69 | To transform a prop, add a `transform: value => transformedValue` function to its descriptor, and use `this.local${PropName}` to get transformed prop. e.g. 70 | 71 | #### 😑 before 72 | 73 | ```html 74 | 77 | 78 | 90 | ``` 91 | 92 | #### 😀 after 93 | 94 | ```html 95 | 98 | 99 | 109 | ``` 110 | 111 | ### Enum-type props 112 | 113 | To define a enum-type prop, add a `enum` array to its descriptor, and its `default` value will be `enum[0]` if the descriptor doesn't contain `default` attribute. e.g. 114 | 115 | #### 😑 before 116 | 117 | ```js 118 | export default { 119 | props: { 120 | size: { 121 | type: String, 122 | default: 'small', 123 | validator: value => ['small', 'large'].indexOf(value) >= 0 124 | } 125 | } 126 | } 127 | ``` 128 | 129 | #### 😀 after 130 | 131 | ```js 132 | export default { 133 | props: { 134 | size: { 135 | type: String, 136 | enum: ['small', 'large'] 137 | } 138 | } 139 | } 140 | ``` 141 | 142 | ### Numeric-type props 143 | 144 | To define a numeric-type prop, add `numeric: true` to its descriptor. Besides, you can set `infinite` to `ture` to allow infinite numbers, which are `-Infinity` and `Infinity`. e.g. 145 | 146 | #### 😑 before 147 | 148 | ```js 149 | export default { 150 | props: { 151 | count: { 152 | type: [Number, String], 153 | default: 0, 154 | validator: value => !isNaN(value - parseFloat(value)) 155 | } 156 | }, 157 | max: { 158 | type: [Number, String], 159 | default: Infinity, 160 | validator: value => value === Infinity || !isNaN(value - parseFloat(value)) 161 | } 162 | } 163 | } 164 | ``` 165 | 166 | #### 😀 after 167 | 168 | ```js 169 | export default { 170 | props: { 171 | count: { 172 | numeric: true, 173 | default: 0 174 | }, 175 | max: { 176 | numeric: true, 177 | infinite: true, 178 | default: Infinity 179 | } 180 | } 181 | } 182 | ``` 183 | 184 | ### Listen for receiving props 185 | 186 | To listen for receiving a prop, add `on: { receive: (newValue, oldValue) => void }` object to its descriptor. e.g. 187 | 188 | #### 😑 before 189 | 190 | ```js 191 | export default { 192 | props: { 193 | count: [Number, String] 194 | }, 195 | watch: { 196 | count: { 197 | immediate: true, 198 | handler(newCount, oldCount) { 199 | console.log(newCount, oldCount) 200 | } 201 | } 202 | } 203 | } 204 | ``` 205 | 206 | #### 😀 after 207 | 208 | ```js 209 | export default { 210 | props: { 211 | count: { 212 | type: [Number, String], 213 | on: { 214 | receive(newCount, oldCount) { 215 | console.log(newCount, oldCount) 216 | } 217 | } 218 | } 219 | } 220 | } 221 | ``` 222 | 223 | ### Two-way data binding props 224 | 225 | To apply two-way data bindings on a prop, add `sync: true` to its descriptor. Then, you can use `this.local${PropName} = newValue` or `this.send${PropName}(newValue)` to send new value to Parent component. 226 | 227 | > If the prop is model prop, it's no need to add `sync: true` to its descriptor. 228 | 229 | #### 😑 before 230 | 231 | ```html 232 | 233 | 236 | 237 | 248 | 249 | 250 | 255 | 256 | 285 | ``` 286 | 287 | #### 😀 after 288 | 289 | ```html 290 | 291 | 294 | 295 | 306 | 307 | 308 | 313 | 314 | 334 | ``` 335 | --------------------------------------------------------------------------------