mirror of
				https://github.com/vuejs/babel-plugin-jsx.git
				synced 2025-10-31 09:22:19 +08:00 
			
		
		
		
	support v-model
This commit is contained in:
		| @@ -26,6 +26,7 @@ | |||||||
|     "@babel/helper-module-imports": "^7.8.3", |     "@babel/helper-module-imports": "^7.8.3", | ||||||
|     "@babel/plugin-syntax-jsx": "^7.8.3", |     "@babel/plugin-syntax-jsx": "^7.8.3", | ||||||
|     "@babel/types": "^7.9.6", |     "@babel/types": "^7.9.6", | ||||||
|  |     "camelcase": "^6.0.0", | ||||||
|     "html-tags": "^3.1.0", |     "html-tags": "^3.1.0", | ||||||
|     "svg-tags": "^1.0.0" |     "svg-tags": "^1.0.0" | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -109,12 +109,16 @@ const transformJSXAttribute = (path, attributesToMerge, directives, injected) => | |||||||
|     const directiveName = name.startsWith('v-') |     const directiveName = name.startsWith('v-') | ||||||
|       ? name.replace('v-', '') |       ? name.replace('v-', '') | ||||||
|       : name.replace(`v${name[1]}`, name[1].toLowerCase()); |       : name.replace(`v${name[1]}`, name[1].toLowerCase()); | ||||||
|  |     if (directiveName === '_model') { | ||||||
|  |       directives.push(getJSXAttributeValue(path)); | ||||||
|  |     } else { | ||||||
|       directives.push(t.arrayExpression([ |       directives.push(t.arrayExpression([ | ||||||
|         t.callExpression(injected.resolveDirective, [ |         t.callExpression(injected.resolveDirective, [ | ||||||
|           t.stringLiteral(directiveName), |           t.stringLiteral(directiveName), | ||||||
|         ]), |         ]), | ||||||
|         getJSXAttributeValue(path), |         getJSXAttributeValue(path), | ||||||
|       ])); |       ])); | ||||||
|  |     } | ||||||
|     return null; |     return null; | ||||||
|   } |   } | ||||||
|   if (rootAttributes.includes(name) || eventRE.test(name)) { |   if (rootAttributes.includes(name) || eventRE.test(name)) { | ||||||
|   | |||||||
							
								
								
									
										209
									
								
								src/babel-sugar-v-model.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										209
									
								
								src/babel-sugar-v-model.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,209 @@ | |||||||
|  | const syntaxJsx = require('@babel/plugin-syntax-jsx').default; | ||||||
|  | const t = require('@babel/types'); | ||||||
|  | const htmlTags = require('html-tags'); | ||||||
|  | const svgTags = require('svg-tags'); | ||||||
|  | const camelCase = require('camelcase'); | ||||||
|  | const { addNamed } = require('@babel/helper-module-imports'); | ||||||
|  |  | ||||||
|  |  | ||||||
|  | const cachedCamelCase = (() => { | ||||||
|  |   const cache = Object.create(null); | ||||||
|  |   return (string) => { | ||||||
|  |     if (!cache[string]) { | ||||||
|  |       cache[string] = camelCase(string); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return cache[string]; | ||||||
|  |   }; | ||||||
|  | })(); | ||||||
|  |  | ||||||
|  | const startsWithCamel = (string, match) => string.startsWith(match) | ||||||
|  |   || string.startsWith(cachedCamelCase(match)); | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Add property to a JSX element | ||||||
|  |  * | ||||||
|  |  * @param t | ||||||
|  |  * @param path JSXOpeningElement | ||||||
|  |  * @param value string | ||||||
|  |  */ | ||||||
|  | const addProp = (path, value) => { | ||||||
|  |   path.node.attributes.push(value); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Get JSX element tag name | ||||||
|  |  * | ||||||
|  |  * @param t | ||||||
|  |  * @param path Path<JSXOpeningElement> | ||||||
|  |  */ | ||||||
|  | const getTagName = (path) => path.get('name.name').node; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Get JSX element type | ||||||
|  |  * | ||||||
|  |  * @param t | ||||||
|  |  * @param path Path<JSXOpeningElement> | ||||||
|  |  */ | ||||||
|  | const getType = (path) => { | ||||||
|  |   const typePath = path | ||||||
|  |     .get('attributes') | ||||||
|  |     .find( | ||||||
|  |       (attributePath) => t.isJSXAttribute(attributePath) | ||||||
|  |         && t.isJSXIdentifier(attributePath.get('name')) | ||||||
|  |         && attributePath.get('name.name').node === 'type' | ||||||
|  |         && t.isStringLiteral(attributePath.get('value')), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |   return typePath ? typePath.get('value.value').node : ''; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Check if a JSXOpeningElement is a component | ||||||
|  |  * | ||||||
|  |  * @param t | ||||||
|  |  * @param path JSXOpeningElement | ||||||
|  |  * @returns boolean | ||||||
|  |  */ | ||||||
|  | const isComponent = (path) => { | ||||||
|  |   const name = path.get('name'); | ||||||
|  |  | ||||||
|  |   if (t.isJSXMemberExpression(name)) { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const tag = name.get('name').node; | ||||||
|  |  | ||||||
|  |   return !htmlTags.includes(tag) && !svgTags.includes(tag); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Transform vModel | ||||||
|  | */ | ||||||
|  | const getModelDirective = (path, state, value) => { | ||||||
|  |   const tag = getTagName(path); | ||||||
|  |   const type = getType(path); | ||||||
|  |  | ||||||
|  |   addProp(path, t.jsxSpreadAttribute( | ||||||
|  |     t.objectExpression([ | ||||||
|  |       t.objectProperty( | ||||||
|  |         t.stringLiteral('onUpdate:modelValue'), | ||||||
|  |         t.arrowFunctionExpression( | ||||||
|  |           [t.identifier('$event')], | ||||||
|  |           t.assignmentExpression('=', value, t.identifier('$event')), | ||||||
|  |         ), | ||||||
|  |       ), | ||||||
|  |     ]), | ||||||
|  |   )); | ||||||
|  |  | ||||||
|  |   if (isComponent(path)) { | ||||||
|  |     addProp(path, t.jsxAttribute(t.jsxIdentifier('modelValue'), t.jsxExpressionContainer(value))); | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let modelToUse; | ||||||
|  |   switch (tag) { | ||||||
|  |     case 'select': | ||||||
|  |       if (!state.vueVModelSelect) { | ||||||
|  |         state.vueVModelSelect = addNamed(path, 'vModelSelect', 'vue'); | ||||||
|  |       } | ||||||
|  |       modelToUse = state.vueVModelSelect; | ||||||
|  |       break; | ||||||
|  |     case 'textarea': | ||||||
|  |       if (!state.vueVModelText) { | ||||||
|  |         state.vueVModelText = addNamed(path, 'vModelText', 'vue'); | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     default: | ||||||
|  |       switch (type) { | ||||||
|  |         case 'checkbox': | ||||||
|  |           if (!state.vueVModelCheckbox) { | ||||||
|  |             state.vueVModelCheckbox = addNamed(path, 'vModelCheckbox', 'vue'); | ||||||
|  |           } | ||||||
|  |           modelToUse = state.vueVModelCheckbox; | ||||||
|  |           break; | ||||||
|  |         case 'radio': | ||||||
|  |           if (!state.vueVModelRadio) { | ||||||
|  |             state.vueVModelRadio = addNamed(path, 'vModelRadio', 'vue'); | ||||||
|  |           } | ||||||
|  |           modelToUse = state.vueVModelRadio; | ||||||
|  |           break; | ||||||
|  |         default: | ||||||
|  |           if (!state.vueVModelText) { | ||||||
|  |             state.vueVModelText = addNamed(path, 'vModelText', 'vue'); | ||||||
|  |           } | ||||||
|  |           modelToUse = state.vueVModelText; | ||||||
|  |       } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return modelToUse; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Parse vModel metadata | ||||||
|  |  * | ||||||
|  |  * @param  path JSXAttribute | ||||||
|  |  * @returns null | Object<{ modifiers: Set<string>, valuePath: Path<Expression>}> | ||||||
|  |  */ | ||||||
|  | const parseVModel = (path) => { | ||||||
|  |   if (t.isJSXNamespacedName(path.get('name')) || !startsWithCamel(path.get('name.name').node, 'v-model')) { | ||||||
|  |     return null; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!t.isJSXExpressionContainer(path.get('value'))) { | ||||||
|  |     throw new Error('You have to use JSX Expression inside your v-model'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const modifiers = path.get('name.name').node.split('_'); | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     modifiers: new Set(modifiers), | ||||||
|  |     value: path.get('value.expression').node, | ||||||
|  |   }; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | module.exports = () => ({ | ||||||
|  |   name: 'babel-sugar-v-model', | ||||||
|  |   inherits: syntaxJsx, | ||||||
|  |   visitor: { | ||||||
|  |     JSXAttribute: { | ||||||
|  |       exit(path, state) { | ||||||
|  |         const parsed = parseVModel(path); | ||||||
|  |         if (!parsed) { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const { modifiers, value } = parsed; | ||||||
|  |  | ||||||
|  |         const parent = path.parentPath; | ||||||
|  |         // v-model={xx} --> v-_model={[directive, xx, void 0, { a: true, b: true }]} | ||||||
|  |         const directive = getModelDirective(parent, state, value); | ||||||
|  |         if (directive) { | ||||||
|  |           path.replaceWith( | ||||||
|  |             t.jsxAttribute( | ||||||
|  |               t.jsxIdentifier('v-_model'), // TODO | ||||||
|  |               t.jsxExpressionContainer( | ||||||
|  |                 t.arrayExpression([ | ||||||
|  |                   directive, | ||||||
|  |                   value, | ||||||
|  |                   modifiers.size && t.unaryExpression('void', t.numericLiteral(0), true), | ||||||
|  |                   modifiers.size && t.objectExpression( | ||||||
|  |                     [...modifiers].map( | ||||||
|  |                       (modifier) => t.objectProperty( | ||||||
|  |                         t.identifier(modifier), | ||||||
|  |                         t.booleanLiteral(true), | ||||||
|  |                       ), | ||||||
|  |                     ), | ||||||
|  |                   ), | ||||||
|  |                 ].filter(Boolean)), | ||||||
|  |               ), | ||||||
|  |             ), | ||||||
|  |           ); | ||||||
|  |         } else { | ||||||
|  |           path.remove(); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
| @@ -1,8 +1,10 @@ | |||||||
|  | const babelSugarVModel = require('./babel-sugar-v-model'); | ||||||
| const babelPluginTransformVueJsx = require('./babel-plugin-transform-vue-jsx'); | const babelPluginTransformVueJsx = require('./babel-plugin-transform-vue-jsx'); | ||||||
| const babelSugarFragment = require('./babel-sugar-fragment'); | const babelSugarFragment = require('./babel-sugar-fragment'); | ||||||
|  |  | ||||||
| module.exports = () => ({ | module.exports = () => ({ | ||||||
|   plugins: [ |   plugins: [ | ||||||
|  |     babelSugarVModel, | ||||||
|     babelPluginTransformVueJsx, |     babelPluginTransformVueJsx, | ||||||
|     babelSugarFragment, |     babelSugarFragment, | ||||||
|   ], |   ], | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user