From 31056616627cd5be14aa9cc75a71e7c5c433f9a0 Mon Sep 17 00:00:00 2001 From: defcc Date: Sat, 22 Oct 2016 14:57:32 +0800 Subject: [PATCH] v-model binding with array. (fix #3958,#3979) (#3988) * fix v-model with array binding * add mutli selects test case * add test case. v-bind with array * add comments * code refactor --- .../web/compiler/directives/model.js | 24 +++++- src/platforms/web/util/model.js | 84 +++++++++++++++++++ .../directives/model-component.spec.js | 11 ++- .../features/directives/model-radio.spec.js | 40 +++++++++ .../features/directives/model-select.spec.js | 38 +++++++++ .../features/directives/model-text.spec.js | 41 +++++++++ 6 files changed, 231 insertions(+), 7 deletions(-) create mode 100644 src/platforms/web/util/model.js diff --git a/src/platforms/web/compiler/directives/model.js b/src/platforms/web/compiler/directives/model.js index 5f8639cd..3dd2c45e 100644 --- a/src/platforms/web/compiler/directives/model.js +++ b/src/platforms/web/compiler/directives/model.js @@ -2,6 +2,7 @@ import { isIE } from 'core/util/env' import { addHandler, addProp, getBindingAttr } from 'compiler/helpers' +import parseModel from 'web/util/model' let warn @@ -79,7 +80,7 @@ function genRadioModel (el: ASTElement, value: string) { } const valueBinding = getBindingAttr(el, 'value') || 'null' addProp(el, 'checked', `_q(${value},${valueBinding})`) - addHandler(el, 'change', `${value}=${valueBinding}`, null, true) + addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true) } function genDefaultModel ( @@ -114,8 +115,8 @@ function genDefaultModel ( ? `$event.target.value${trim ? '.trim()' : ''}` : `$event` let code = number || type === 'number' - ? `${value}=_n(${valueExpression})` - : `${value}=${valueExpression}` + ? genAssignmentCode(value, `_n(${valueExpression})`) + : genAssignmentCode(value, valueExpression) if (isNative && needCompositionGuard) { code = `if($event.target.composing)return;${code}` } @@ -136,10 +137,13 @@ function genSelect (el: ASTElement, value: string) { if (process.env.NODE_ENV !== 'production') { el.children.some(checkOptionWarning) } - const code = `${value}=Array.prototype.filter` + + + const assignment = `Array.prototype.filter` + `.call($event.target.options,function(o){return o.selected})` + `.map(function(o){return "_value" in o ? o._value : o.value})` + (el.attrsMap.multiple == null ? '[0]' : '') + + const code = genAssignmentCode(value, assignment) addHandler(el, 'change', code, null, true) } @@ -156,3 +160,15 @@ function checkOptionWarning (option: any): boolean { } return false } + +function genAssignmentCode (value: string, assignment: string): string { + const modelRs = parseModel(value) + if (modelRs.idx === null) { + return `${value}=${assignment}` + } else { + return `var $$exp = ${modelRs.exp}, $$idx = ${modelRs.idx};` + + `if (!Array.isArray($$exp)){` + + `${value}=${assignment}}` + + `else{$$exp.splice($$idx, 1, ${assignment})}` + } +} diff --git a/src/platforms/web/util/model.js b/src/platforms/web/util/model.js new file mode 100644 index 00000000..fdf6a5ac --- /dev/null +++ b/src/platforms/web/util/model.js @@ -0,0 +1,84 @@ +/* @flow */ + +let len, str, chr, index, expressionPos, expressionEndPos + +/** + * parse directive model to do the array update transform. a[idx] = val => $$a.splice($$idx, 1, val) + * + * for loop possible cases: + * + * - test + * - test[idx] + * - test[test1[idx]] + * - test["a"][idx] + * - xxx.test[a[a].test1[idx]] + * - test.xxx.a["asa"][test1[idx]] + * + */ + +export default function parseModel (val: string): Object { + str = val + len = str.length + index = expressionPos = expressionEndPos = 0 + + if (val.indexOf('[') < 0) { + return { + exp: val, + idx: null + } + } + + while (!eof()) { + chr = next() + if (isStringStart(chr)) { + parseString(chr) + } else if (chr === 0x5B) { + parseBracket(chr) + } + } + + return { + exp: val.substring(0, expressionPos), + idx: val.substring(expressionPos + 1, expressionEndPos) + } +} + +function next (): number { + return str.charCodeAt(++index) +} + +function eof (): boolean { + return index >= len +} + +function isStringStart (chr: number): boolean { + return chr === 0x22 || chr === 0x27 +} + +function parseBracket (chr: number): void { + let inBracket = 1 + expressionPos = index + while (!eof()) { + chr = next() + if (isStringStart(chr)) { + parseString(chr) + continue + } + if (chr === 0x5B) inBracket++ + if (chr === 0x5D) inBracket-- + if (inBracket === 0) { + expressionEndPos = index + break + } + } +} + +function parseString (chr: number): void { + const stringQuote = chr + while (!eof()) { + chr = next() + if (chr === stringQuote) { + break + } + } +} diff --git a/test/unit/features/directives/model-component.spec.js b/test/unit/features/directives/model-component.spec.js index 1739e780..622deed7 100644 --- a/test/unit/features/directives/model-component.spec.js +++ b/test/unit/features/directives/model-component.spec.js @@ -2,14 +2,18 @@ import Vue from 'vue' describe('Directive v-model component', () => { it('should work', done => { + const spy = jasmine.createSpy() const vm = new Vue({ data: { - msg: 'hello' + msg: ['hello'] + }, + watch: { + msg: spy }, template: `

{{ msg }}

- +
@@ -40,7 +44,8 @@ describe('Directive v-model component', () => { input.value = 'world' triggerEvent(input, 'input') }).then(() => { - expect(vm.msg).toBe('world') + expect(vm.msg).toEqual(['world']) + expect(spy).toHaveBeenCalled() }).then(() => { document.body.removeChild(vm.$el) vm.$destroy() diff --git a/test/unit/features/directives/model-radio.spec.js b/test/unit/features/directives/model-radio.spec.js index 175f0cc8..b52456d4 100644 --- a/test/unit/features/directives/model-radio.spec.js +++ b/test/unit/features/directives/model-radio.spec.js @@ -85,6 +85,46 @@ describe('Directive v-model radio', () => { }).then(done) }) + it('multiple radios ', (done) => { + const spy = jasmine.createSpy() + const vm = new Vue({ + data: { + selections: ['a', '1'], + radioList: [ + { + name: 'questionA', + data: ['a', 'b', 'c'] + }, + { + name: 'questionB', + data: ['1', '2'] + } + ] + }, + watch: { + selections: spy + }, + template: + '
' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '
' + + '
' + + '
' + }).$mount() + document.body.appendChild(vm.$el) + var inputs = vm.$el.getElementsByTagName('input') + inputs[1].click() + waitForUpdate(() => { + expect(vm.selections).toEqual(['b', '1']) + expect(spy).toHaveBeenCalled() + }).then(done) + }) + it('warn inline checked', () => { const vm = new Vue({ template: ``, diff --git a/test/unit/features/directives/model-select.spec.js b/test/unit/features/directives/model-select.spec.js index dad45168..7de1f0d3 100644 --- a/test/unit/features/directives/model-select.spec.js +++ b/test/unit/features/directives/model-select.spec.js @@ -263,6 +263,44 @@ describe('Directive v-model select', () => { }).then(done) }) + it('multiple selects', (done) => { + const spy = jasmine.createSpy() + const vm = new Vue({ + data: { + selections: ['', ''], + selectBoxes: [ + [ + { value: 'foo', text: 'foo' }, + { value: 'bar', text: 'bar' } + ], + [ + { value: 'day', text: 'day' }, + { value: 'night', text: 'night' } + ] + ] + }, + watch: { + selections: spy + }, + template: + '
' + + '' + + '{{selections}}' + + '
' + }).$mount() + document.body.appendChild(vm.$el) + var selects = vm.$el.getElementsByTagName('select') + var select0 = selects[0] + select0.options[0].selected = true + triggerEvent(select0, 'change') + waitForUpdate(() => { + expect(spy).toHaveBeenCalled() + expect(vm.selections).toEqual(['foo', '']) + }).then(done) + }) + it('should warn inline selected', () => { const vm = new Vue({ data: { diff --git a/test/unit/features/directives/model-text.spec.js b/test/unit/features/directives/model-text.spec.js index 23ccb30e..f4ce9e45 100644 --- a/test/unit/features/directives/model-text.spec.js +++ b/test/unit/features/directives/model-text.spec.js @@ -64,6 +64,47 @@ describe('Directive v-model text', () => { expect(vm.test).toBe('what') }) + it('multiple inputs', (done) => { + const spy = jasmine.createSpy() + const vm = new Vue({ + data: { + selections: [[1, 2, 3], [4, 5]], + inputList: [ + { + name: 'questionA', + data: ['a', 'b', 'c'] + }, + { + name: 'questionB', + data: ['1', '2'] + } + ] + }, + watch: { + selections: spy + }, + template: + '
' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '
' + + '
' + + '{{selections}}' + + '
' + }).$mount() + var inputs = vm.$el.getElementsByTagName('input') + inputs[1].value = 'test' + triggerEvent(inputs[1], 'input') + waitForUpdate(() => { + expect(spy).toHaveBeenCalled() + expect(vm.selections).toEqual([[1, 'test', 3], [4, 5]]) + }).then(done) + }) + if (isIE9) { it('IE9 selectionchange', done => { const vm = new Vue({