mirror of
https://github.com/vuejs/babel-plugin-jsx.git
synced 2025-01-11 00:39:10 +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:
parent
0aeb92072b
commit
604d53b956
@ -203,7 +203,7 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
|
|||||||
}
|
}
|
||||||
if (isDirective(name)) {
|
if (isDirective(name)) {
|
||||||
const {
|
const {
|
||||||
directive, modifiers, value, arg, directiveName,
|
directive, modifiers, values, args, directiveName,
|
||||||
} = parseDirectives({
|
} = parseDirectives({
|
||||||
tag,
|
tag,
|
||||||
isComponent,
|
isComponent,
|
||||||
@ -212,8 +212,6 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
|
|||||||
state,
|
state,
|
||||||
value: attributeValue,
|
value: attributeValue,
|
||||||
});
|
});
|
||||||
const argVal = (arg as t.StringLiteral)?.value;
|
|
||||||
const propName = argVal || 'modelValue';
|
|
||||||
|
|
||||||
if (directiveName === 'slots') {
|
if (directiveName === 'slots') {
|
||||||
slots = attributeValue as Slots;
|
slots = attributeValue as Slots;
|
||||||
@ -221,52 +219,59 @@ const buildProps = (path: NodePath<t.JSXElement>, state: State) => {
|
|||||||
}
|
}
|
||||||
if (directive) {
|
if (directive) {
|
||||||
directives.push(t.arrayExpression(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') {
|
} else if (directiveName === 'html') {
|
||||||
properties.push(t.objectProperty(
|
properties.push(t.objectProperty(
|
||||||
t.stringLiteral('innerHTML'),
|
t.stringLiteral('innerHTML'),
|
||||||
value as any,
|
values[0] as any,
|
||||||
));
|
));
|
||||||
dynamicPropNames.add('innerHTML');
|
dynamicPropNames.add('innerHTML');
|
||||||
} else if (directiveName === 'text') {
|
} else if (directiveName === 'text') {
|
||||||
properties.push(t.objectProperty(
|
properties.push(t.objectProperty(
|
||||||
t.stringLiteral('textContent'),
|
t.stringLiteral('textContent'),
|
||||||
value as any,
|
values[0] as any,
|
||||||
));
|
));
|
||||||
dynamicPropNames.add('textContent');
|
dynamicPropNames.add('textContent');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (directiveName === 'model' && value) {
|
if (['models', 'model'].includes(directiveName)) {
|
||||||
properties.push(t.objectProperty(
|
values.forEach((value, index) => {
|
||||||
t.stringLiteral(`onUpdate:${propName}`),
|
const argVal = args[index].value;
|
||||||
t.arrowFunctionExpression(
|
const propName = argVal === 'model' ? 'modelValue' : argVal;
|
||||||
[t.identifier('$event')],
|
|
||||||
t.assignmentExpression('=', value as any, t.identifier('$event')),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
if (name.match(xlinkRE)) {
|
if (name.match(xlinkRE)) {
|
||||||
|
@ -24,16 +24,14 @@ const getType = (path: NodePath<t.JSXOpeningElement>) => {
|
|||||||
return typePath ? typePath.get('value').node : null;
|
return typePath ? typePath.get('value').node : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseModifiers = (value: t.Expression) => {
|
const parseModifiers = (value: t.ArrayExpression): string[] => (
|
||||||
let modifiers: string[] = [];
|
t.isArrayExpression(value)
|
||||||
if (t.isArrayExpression(value)) {
|
? value.elements
|
||||||
modifiers = value.elements
|
.map((el) => (t.isStringLiteral(el) ? el.value : ''))
|
||||||
.map((el) => (t.isStringLiteral(el) ? el.value : '')).filter(Boolean);
|
.filter(Boolean)
|
||||||
}
|
: []);
|
||||||
return modifiers;
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseDirectives = (args: {
|
const parseDirectives = (params: {
|
||||||
name: string,
|
name: string,
|
||||||
path: NodePath<t.JSXAttribute>,
|
path: NodePath<t.JSXAttribute>,
|
||||||
value: t.StringLiteral | t.Expression | null,
|
value: t.StringLiteral | t.Expression | null,
|
||||||
@ -43,48 +41,75 @@ const parseDirectives = (args: {
|
|||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
name, path, value, state, tag, isComponent,
|
name, path, value, state, tag, isComponent,
|
||||||
} = args;
|
} = params;
|
||||||
let modifiers: string[] = name.split('_');
|
const args: t.StringLiteral[] = [];
|
||||||
let arg;
|
const vals: t.Expression[] = [];
|
||||||
let val;
|
const modifiersSet: Set<string>[] = [];
|
||||||
|
const underscoreModifiers = name.split('_');
|
||||||
const directiveName: string = modifiers.shift()
|
const directiveName: string = underscoreModifiers.shift()
|
||||||
?.replace(/^v/, '')
|
?.replace(/^v/, '')
|
||||||
.replace(/^-/, '')
|
.replace(/^-/, '')
|
||||||
.replace(/^\S/, (s: string) => s.toLowerCase()) || '';
|
.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');
|
throw new Error('You have to use JSX Expression inside your v-model');
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldResolve = !['html', 'text', 'model'].includes(directiveName)
|
if (isVModels && !isComponent) {
|
||||||
|| (directiveName === 'model' && !isComponent);
|
throw new Error('v-models can only use in custom components');
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
return {
|
||||||
directiveName,
|
directiveName,
|
||||||
modifiers: modifiersSet,
|
modifiers: modifiersSet,
|
||||||
value: val || value,
|
values: vals.length ? vals : [value],
|
||||||
arg,
|
args,
|
||||||
directive: shouldResolve ? [
|
directive: shouldResolve ? [
|
||||||
resolveDirective(path, state, tag, directiveName),
|
resolveDirective(path, state, tag, directiveName),
|
||||||
val || value,
|
vals[0] || value,
|
||||||
!!modifiersSet.size && t.unaryExpression('void', t.numericLiteral(0), true),
|
!!modifiersSet[0]?.size && t.unaryExpression('void', t.numericLiteral(0), true),
|
||||||
!!modifiersSet.size && t.objectExpression(
|
!!modifiersSet[0]?.size && t.objectExpression(
|
||||||
[...modifiersSet].map(
|
[...modifiersSet[0]].map(
|
||||||
(modifier) => t.objectProperty(
|
(modifier) => t.objectProperty(
|
||||||
t.identifier(modifier),
|
t.identifier(modifier),
|
||||||
t.booleanLiteral(true),
|
t.booleanLiteral(true),
|
||||||
|
@ -171,6 +171,23 @@ createVNode(\\"h1\\", {
|
|||||||
}, null, 8, [\\"innerHTML\\"]);"
|
}, 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`] = `
|
exports[`vText: vText 1`] = `
|
||||||
"import { createVNode } from \\"vue\\";
|
"import { createVNode } from \\"vue\\";
|
||||||
createVNode(\\"div\\", {
|
createVNode(\\"div\\", {
|
||||||
|
@ -134,6 +134,10 @@ const tests = [
|
|||||||
a = <A>{a}</A>;
|
a = <A>{a}</A>;
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'vModels',
|
||||||
|
from: '<C v-models={[[foo, ["modifier"]], [bar, "bar", ["modifier1", "modifier2"]]]} />',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
tests.forEach((
|
tests.forEach((
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount, mount } from '@vue/test-utils';
|
||||||
import { VNode } from '@vue/runtime-dom';
|
import { defineComponent, VNode } from '@vue/runtime-dom';
|
||||||
|
|
||||||
test('input[type="checkbox"] should work', async () => {
|
test('input[type="checkbox"] should work', async () => {
|
||||||
const wrapper = shallowMount({
|
const wrapper = shallowMount({
|
||||||
@ -164,3 +164,68 @@ test('dynamic type should work', async () => {
|
|||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
expect(wrapper.vm.$el.checked).toBe(false);
|
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>');
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user