mirror of
https://github.com/vuejs/babel-plugin-jsx.git
synced 2025-01-10 16:29:12 +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)) {
|
||||
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>');
|
||||
});
|
Loading…
Reference in New Issue
Block a user