mirror of
				https://github.com/vuejs/babel-plugin-jsx.git
				synced 2025-10-31 01:12:17 +08:00 
			
		
		
		
	feat: support v-models (#140)
* feat: support multiple bindings * refactor: optimize code * test: add underscore modifier case * refactor: modify value to values * refactor: remove underscore modifier support * refactor: optimize v-models * refactor: optimize code
This commit is contained in:
		| @@ -203,7 +203,7 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => { | ||||
|         } | ||||
|         if (isDirective(name)) { | ||||
|           const { | ||||
|             directive, modifiers, value, arg, directiveName, | ||||
|             directive, modifiers, values, args, directiveName, | ||||
|           } = parseDirectives({ | ||||
|             tag, | ||||
|             isComponent, | ||||
| @@ -212,8 +212,6 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => { | ||||
|             state, | ||||
|             value: attributeValue, | ||||
|           }); | ||||
|           const argVal = (arg as t.StringLiteral)?.value; | ||||
|           const propName = argVal || 'modelValue'; | ||||
|  | ||||
|           if (directiveName === 'slots') { | ||||
|             slots = attributeValue as Slots; | ||||
| @@ -221,52 +219,59 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => { | ||||
|           } | ||||
|           if (directive) { | ||||
|             directives.push(t.arrayExpression(directive)); | ||||
|           } else if (directiveName === 'model') { | ||||
|             // must be v-model and is a component | ||||
|             properties.push(t.objectProperty( | ||||
|               arg || t.stringLiteral('modelValue'), | ||||
|               value as any, | ||||
|             )); | ||||
|  | ||||
|             dynamicPropNames.add(propName); | ||||
|  | ||||
|             if (modifiers.size) { | ||||
|               properties.push(t.objectProperty( | ||||
|                 t.stringLiteral(`${argVal || 'model'}Modifiers`), | ||||
|                 t.objectExpression( | ||||
|                   [...modifiers].map((modifier) => ( | ||||
|                     t.objectProperty( | ||||
|                       t.stringLiteral(modifier), | ||||
|                       t.booleanLiteral(true), | ||||
|                     ) | ||||
|                   )), | ||||
|                 ), | ||||
|               )); | ||||
|             } | ||||
|           } else if (directiveName === 'html') { | ||||
|             properties.push(t.objectProperty( | ||||
|               t.stringLiteral('innerHTML'), | ||||
|               value as any, | ||||
|               values[0] as any, | ||||
|             )); | ||||
|             dynamicPropNames.add('innerHTML'); | ||||
|           } else if (directiveName === 'text') { | ||||
|             properties.push(t.objectProperty( | ||||
|               t.stringLiteral('textContent'), | ||||
|               value as any, | ||||
|               values[0] as any, | ||||
|             )); | ||||
|             dynamicPropNames.add('textContent'); | ||||
|           } | ||||
|  | ||||
|           if (directiveName === 'model' && value) { | ||||
|             properties.push(t.objectProperty( | ||||
|               t.stringLiteral(`onUpdate:${propName}`), | ||||
|               t.arrowFunctionExpression( | ||||
|                 [t.identifier('$event')], | ||||
|                 t.assignmentExpression('=', value as any, t.identifier('$event')), | ||||
|               ), | ||||
|             )); | ||||
|           if (['models', 'model'].includes(directiveName)) { | ||||
|             values.forEach((value, index) => { | ||||
|               const argVal = args[index].value; | ||||
|               const propName = argVal === 'model' ? 'modelValue' : argVal; | ||||
|  | ||||
|             dynamicPropNames.add(`onUpdate:${propName}`); | ||||
|               // must be v-model or v-models and is a component | ||||
|               if (!directive) { | ||||
|                 properties.push( | ||||
|                   t.objectProperty(t.stringLiteral(propName), value as any), | ||||
|                 ); | ||||
|                 dynamicPropNames.add(propName); | ||||
|  | ||||
|                 if (modifiers[index]?.size) { | ||||
|                   properties.push( | ||||
|                     t.objectProperty( | ||||
|                       t.stringLiteral(`${argVal}Modifiers`), | ||||
|                       t.objectExpression( | ||||
|                         [...modifiers[index]].map((modifier) => t.objectProperty( | ||||
|                           t.stringLiteral(modifier), | ||||
|                           t.booleanLiteral(true), | ||||
|                         )), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ); | ||||
|                 } | ||||
|               } | ||||
|  | ||||
|               properties.push( | ||||
|                 t.objectProperty( | ||||
|                   t.stringLiteral(`onUpdate:${propName}`), | ||||
|                   t.arrowFunctionExpression( | ||||
|                     [t.identifier('$event')], | ||||
|                     t.assignmentExpression('=', value as any, t.identifier('$event')), | ||||
|                   ), | ||||
|                 ), | ||||
|               ); | ||||
|  | ||||
|               dynamicPropNames.add(`onUpdate:${propName}`); | ||||
|             }); | ||||
|           } | ||||
|         } else { | ||||
|           if (name.match(xlinkRE)) { | ||||
|   | ||||
| @@ -24,16 +24,14 @@ const getType = (path: NodePath<t.JSXOpeningElement>) => { | ||||
|   return typePath ? typePath.get('value').node : null; | ||||
| }; | ||||
|  | ||||
| const parseModifiers = (value: t.Expression) => { | ||||
|   let modifiers: string[] = []; | ||||
|   if (t.isArrayExpression(value)) { | ||||
|     modifiers = value.elements | ||||
|       .map((el) => (t.isStringLiteral(el) ? el.value : '')).filter(Boolean); | ||||
|   } | ||||
|   return modifiers; | ||||
| }; | ||||
| const parseModifiers = (value: t.ArrayExpression): string[] => ( | ||||
|   t.isArrayExpression(value) | ||||
|     ? value.elements | ||||
|       .map((el) => (t.isStringLiteral(el) ? el.value : '')) | ||||
|       .filter(Boolean) | ||||
|     : []); | ||||
|  | ||||
| const parseDirectives = (args: { | ||||
| const parseDirectives = (params: { | ||||
|   name: string, | ||||
|   path: NodePath<t.JSXAttribute>, | ||||
|   value: t.StringLiteral | t.Expression | null, | ||||
| @@ -43,48 +41,75 @@ const parseDirectives = (args: { | ||||
| }) => { | ||||
|   const { | ||||
|     name, path, value, state, tag, isComponent, | ||||
|   } = args; | ||||
|   let modifiers: string[] = name.split('_'); | ||||
|   let arg; | ||||
|   let val; | ||||
|  | ||||
|   const directiveName: string = modifiers.shift() | ||||
|   } = params; | ||||
|   const args: t.StringLiteral[] = []; | ||||
|   const vals: t.Expression[] = []; | ||||
|   const modifiersSet: Set<string>[] = []; | ||||
|   const underscoreModifiers = name.split('_'); | ||||
|   const directiveName: string = underscoreModifiers.shift() | ||||
|     ?.replace(/^v/, '') | ||||
|     .replace(/^-/, '') | ||||
|     .replace(/^\S/, (s: string) => s.toLowerCase()) || ''; | ||||
|  | ||||
|   if (directiveName === 'model' && !t.isJSXExpressionContainer(path.get('value'))) { | ||||
|   const isVModels = directiveName === 'models'; | ||||
|   const isVModel = directiveName === 'model'; | ||||
|   if (isVModel && !t.isJSXExpressionContainer(path.get('value'))) { | ||||
|     throw new Error('You have to use JSX Expression inside your v-model'); | ||||
|   } | ||||
|  | ||||
|   const shouldResolve = !['html', 'text', 'model'].includes(directiveName) | ||||
|     || (directiveName === 'model' && !isComponent); | ||||
|  | ||||
|   if (t.isArrayExpression(value)) { | ||||
|     const { elements } = value; | ||||
|     const [first, second, third] = elements; | ||||
|     if (t.isStringLiteral(second)) { | ||||
|       arg = second; | ||||
|       modifiers = parseModifiers(third as t.Expression); | ||||
|     } else if (second) { | ||||
|       modifiers = parseModifiers(second as t.Expression); | ||||
|     } | ||||
|     val = first; | ||||
|   if (isVModels && !isComponent) { | ||||
|     throw new Error('v-models can only use in custom components'); | ||||
|   } | ||||
|  | ||||
|   const modifiersSet = new Set(modifiers); | ||||
|   const shouldResolve = !['html', 'text', 'model', 'models'].includes(directiveName) | ||||
|     || (isVModel && !isComponent); | ||||
|  | ||||
|   if (['models', 'model'].includes(directiveName)) { | ||||
|     if (t.isArrayExpression(value)) { | ||||
|       const elementsList = isVModels ? value.elements! : [value]; | ||||
|  | ||||
|       elementsList.forEach((element) => { | ||||
|         if (isVModels && !t.isArrayExpression(element)) { | ||||
|           throw new Error('You should pass a Two-dimensional Arrays to v-models'); | ||||
|         } | ||||
|  | ||||
|         const { elements } = element as t.ArrayExpression; | ||||
|         const [first, second, third] = elements; | ||||
|         let modifiers = underscoreModifiers; | ||||
|  | ||||
|         if (t.isStringLiteral(second)) { | ||||
|           args.push(second); | ||||
|           modifiers = parseModifiers(third as t.ArrayExpression); | ||||
|         } else if (t.isArrayExpression(second)) { | ||||
|           args.push(t.stringLiteral('model')); | ||||
|           modifiers = parseModifiers(second); | ||||
|         } else { | ||||
|           // work as v-model={[value]} or v-models={[[value]]} | ||||
|           args.push(t.stringLiteral('model')); | ||||
|         } | ||||
|         modifiersSet.push(new Set(modifiers)); | ||||
|         vals.push(first as t.Expression); | ||||
|       }); | ||||
|     } else if (isVModel) { | ||||
|       // work as v-model={value} | ||||
|       args.push(t.stringLiteral('model')); | ||||
|       modifiersSet.push(new Set(underscoreModifiers)); | ||||
|     } | ||||
|   } else { | ||||
|     modifiersSet.push(new Set(underscoreModifiers)); | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     directiveName, | ||||
|     modifiers: modifiersSet, | ||||
|     value: val || value, | ||||
|     arg, | ||||
|     values: vals.length ? vals : [value], | ||||
|     args, | ||||
|     directive: shouldResolve ? [ | ||||
|       resolveDirective(path, state, tag, directiveName), | ||||
|       val || value, | ||||
|       !!modifiersSet.size && t.unaryExpression('void', t.numericLiteral(0), true), | ||||
|       !!modifiersSet.size && t.objectExpression( | ||||
|         [...modifiersSet].map( | ||||
|       vals[0] || value, | ||||
|       !!modifiersSet[0]?.size && t.unaryExpression('void', t.numericLiteral(0), true), | ||||
|       !!modifiersSet[0]?.size && t.objectExpression( | ||||
|         [...modifiersSet[0]].map( | ||||
|           (modifier) => t.objectProperty( | ||||
|             t.identifier(modifier), | ||||
|             t.booleanLiteral(true), | ||||
|   | ||||
| @@ -171,6 +171,23 @@ createVNode(\\"h1\\", { | ||||
| }, null, 8, [\\"innerHTML\\"]);" | ||||
| `; | ||||
|  | ||||
| exports[`vModels: vModels 1`] = ` | ||||
| "import { resolveComponent, createVNode } from \\"vue\\"; | ||||
| createVNode(resolveComponent(\\"C\\"), { | ||||
|   \\"modelValue\\": foo, | ||||
|   \\"modelModifiers\\": { | ||||
|     \\"modifier\\": true | ||||
|   }, | ||||
|   \\"onUpdate:modelValue\\": $event => foo = $event, | ||||
|   \\"bar\\": bar, | ||||
|   \\"barModifiers\\": { | ||||
|     \\"modifier1\\": true, | ||||
|     \\"modifier2\\": true | ||||
|   }, | ||||
|   \\"onUpdate:bar\\": $event => bar = $event | ||||
| }, null, 8, [\\"modelValue\\", \\"onUpdate:modelValue\\", \\"bar\\", \\"onUpdate:bar\\"]);" | ||||
| `; | ||||
|  | ||||
| exports[`vText: vText 1`] = ` | ||||
| "import { createVNode } from \\"vue\\"; | ||||
| createVNode(\\"div\\", { | ||||
|   | ||||
| @@ -134,6 +134,10 @@ const tests = [ | ||||
|       a = <A>{a}</A>; | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     name: 'vModels', | ||||
|     from: '<C v-models={[[foo, ["modifier"]], [bar, "bar", ["modifier1", "modifier2"]]]} />', | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| tests.forEach(( | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { shallowMount } from '@vue/test-utils'; | ||||
| import { VNode } from '@vue/runtime-dom'; | ||||
| import { shallowMount, mount } from '@vue/test-utils'; | ||||
| import { defineComponent, VNode } from '@vue/runtime-dom'; | ||||
|  | ||||
| test('input[type="checkbox"] should work', async () => { | ||||
|   const wrapper = shallowMount({ | ||||
| @@ -164,3 +164,68 @@ test('dynamic type should work', async () => { | ||||
|   await wrapper.vm.$nextTick(); | ||||
|   expect(wrapper.vm.$el.checked).toBe(false); | ||||
| }); | ||||
|  | ||||
| test('underscore modifier should work', async () => { | ||||
|   const wrapper = shallowMount({ | ||||
|     data: () => ({ | ||||
|       test: 'b', | ||||
|     }), | ||||
|     render() { | ||||
|       return <input v-model_lazy={this.test} />; | ||||
|     }, | ||||
|   }); | ||||
|   const el = wrapper.vm.$el; | ||||
|  | ||||
|   expect(el.value).toBe('b'); | ||||
|   expect(wrapper.vm.test).toBe('b'); | ||||
|   el.value = 'c'; | ||||
|   await wrapper.trigger('input'); | ||||
|   expect(wrapper.vm.test).toBe('b'); | ||||
|   el.value = 'c'; | ||||
|   await wrapper.trigger('change'); | ||||
|   expect(wrapper.vm.test).toBe('c'); | ||||
| }); | ||||
|  | ||||
| test('underscore modifier should work in custom component', async () => { | ||||
|   const Child = defineComponent({ | ||||
|     props: { | ||||
|       modelValue: { | ||||
|         type: Number, | ||||
|         default: 0, | ||||
|       }, | ||||
|       modelModifiers: { | ||||
|         default: () => ({ double: false }), | ||||
|       }, | ||||
|     }, | ||||
|     setup(props, { emit }) { | ||||
|       const handleClick = () => { | ||||
|         emit('update:modelValue', 3); | ||||
|       }; | ||||
|       return () => ( | ||||
|         <div onClick={handleClick}> | ||||
|           {props.modelModifiers.double | ||||
|             ? props.modelValue * 2 | ||||
|             : props.modelValue} | ||||
|         </div> | ||||
|       ); | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   const wrapper = mount({ | ||||
|     data() { | ||||
|       return { | ||||
|         foo: 1, | ||||
|       }; | ||||
|     }, | ||||
|     render() { | ||||
|       return <Child v-model_double={this.foo} />; | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   expect(wrapper.html()).toBe('<div>2</div>'); | ||||
|   wrapper.vm.$data.foo += 1; | ||||
|   await wrapper.vm.$nextTick(); | ||||
|   expect(wrapper.html()).toBe('<div>4</div>'); | ||||
|   await wrapper.trigger('click'); | ||||
|   expect(wrapper.html()).toBe('<div>6</div>'); | ||||
| }); | ||||
|   | ||||
							
								
								
									
										123
									
								
								packages/babel-plugin-jsx/test/v-models.test.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								packages/babel-plugin-jsx/test/v-models.test.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| import { mount } from '@vue/test-utils'; | ||||
| import { defineComponent } from 'vue'; | ||||
|  | ||||
| test('single value binding should work', async () => { | ||||
|   const Child = defineComponent({ | ||||
|     props: { | ||||
|       foo: Number, | ||||
|     }, | ||||
|     setup(props, { emit }) { | ||||
|       const handleClick = () => { | ||||
|         emit('update:foo', 3); | ||||
|       }; | ||||
|       return () => <div onClick={handleClick}>{props.foo}</div>; | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   const wrapper = mount({ | ||||
|     data() { | ||||
|       return { | ||||
|         foo: 1, | ||||
|       }; | ||||
|     }, | ||||
|     render() { | ||||
|       return <Child v-models={[[this.foo, 'foo']]} />; | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   expect(wrapper.html()).toBe('<div>1</div>'); | ||||
|   wrapper.vm.$data.foo += 1; | ||||
|   await wrapper.vm.$nextTick(); | ||||
|   expect(wrapper.html()).toBe('<div>2</div>'); | ||||
|   await wrapper.trigger('click'); | ||||
|   expect(wrapper.html()).toBe('<div>3</div>'); | ||||
| }); | ||||
|  | ||||
| test('multiple values binding should work', async () => { | ||||
|   const Child = defineComponent({ | ||||
|     props: { | ||||
|       foo: Number, | ||||
|       bar: Number, | ||||
|     }, | ||||
|     setup(props, { emit }) { | ||||
|       const handleClick = () => { | ||||
|         emit('update:foo', 3); | ||||
|         emit('update:bar', 2); | ||||
|       }; | ||||
|       return () => ( | ||||
|         <div onClick={handleClick}> | ||||
|           {props.foo},{props.bar} | ||||
|         </div> | ||||
|       ); | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   const wrapper = mount({ | ||||
|     data() { | ||||
|       return { | ||||
|         foo: 1, | ||||
|         bar: 0, | ||||
|       }; | ||||
|     }, | ||||
|     render() { | ||||
|       return ( | ||||
|         <Child | ||||
|           v-models={[ | ||||
|             [this.foo, 'foo'], | ||||
|             [this.bar, 'bar'], | ||||
|           ]} | ||||
|         /> | ||||
|       ); | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   expect(wrapper.html()).toBe('<div>1,0</div>'); | ||||
|   wrapper.vm.$data.foo += 1; | ||||
|   wrapper.vm.$data.bar += 1; | ||||
|   await wrapper.vm.$nextTick(); | ||||
|   expect(wrapper.html()).toBe('<div>2,1</div>'); | ||||
|   await wrapper.trigger('click'); | ||||
|   expect(wrapper.html()).toBe('<div>3,2</div>'); | ||||
| }); | ||||
|  | ||||
| test('modifier should work', async () => { | ||||
|   const Child = defineComponent({ | ||||
|     props: { | ||||
|       foo: { | ||||
|         type: Number, | ||||
|         default: 0, | ||||
|       }, | ||||
|       fooModifiers: { | ||||
|         default: () => ({ double: false }), | ||||
|       }, | ||||
|     }, | ||||
|     setup(props, { emit }) { | ||||
|       const handleClick = () => { | ||||
|         emit('update:foo', 3); | ||||
|       }; | ||||
|       return () => ( | ||||
|         <div onClick={handleClick}> | ||||
|           {props.fooModifiers.double ? props.foo * 2 : props.foo} | ||||
|         </div> | ||||
|       ); | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   const wrapper = mount({ | ||||
|     data() { | ||||
|       return { | ||||
|         foo: 1, | ||||
|       }; | ||||
|     }, | ||||
|     render() { | ||||
|       return <Child v-models={[[this.foo, 'foo', ['double']]]} />; | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   expect(wrapper.html()).toBe('<div>2</div>'); | ||||
|   wrapper.vm.$data.foo += 1; | ||||
|   await wrapper.vm.$nextTick(); | ||||
|   expect(wrapper.html()).toBe('<div>4</div>'); | ||||
|   await wrapper.trigger('click'); | ||||
|   expect(wrapper.html()).toBe('<div>6</div>'); | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user