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:
John60676 2020-10-28 18:42:43 +08:00 committed by GitHub
parent 0aeb92072b
commit 604d53b956
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 313 additions and 74 deletions

View File

@ -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)) {

View File

@ -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),

View File

@ -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\\", {

View File

@ -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((

View File

@ -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>');
});

View 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>');
});